Skip to content

ref(react, browser): make @sentry/react tree-shakeable by default#22168

Closed
anonrig wants to merge 8 commits into
getsentry:developfrom
anonrig:feat/react-tree-shakeable-exports
Closed

ref(react, browser): make @sentry/react tree-shakeable by default#22168
anonrig wants to merge 8 commits into
getsentry:developfrom
anonrig:feat/react-tree-shakeable-exports

Conversation

@anonrig

@anonrig anonrig commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Stop star-exporting the full @sentry/browser surface from @sentry/react so unused optional features are not forced into client bundles.

Problem

Apps that both statically import from @sentry/react and dynamically load it (common for deferring tracing after hydration) can end up shipping the entire browser barrel on the critical path.

With Rolldown (Vite 8), destructuring a dynamic import namespace is treated as using every export:

void import('@sentry/react').then((sentry) => {
  const { addIntegration, tanstackRouterBrowserTracingIntegration } = sentry;
  // ...
});

Because @sentry/react did export * from '@sentry/browser', that pulled Session Replay (~265KB), Feedback, AI instrumenters, feature-flag integrations, and unused React Router/Redux integrations into the main client chunk even when the app never referenced them.

Measured on a production-like Rolldown graph (static init + idle dynamic import):

Pattern Bundle (approx.)
Before (full barrel + destructure) ~1000KB
After (curated root + subpath for TanStack) ~430–475KB, no @sentry/replay / rrweb

Solution

  1. @sentry/browser/core — slim entry for error monitoring, default integrations, and common tracing/filtering APIs (no Replay, Feedback, AI, feature flags, profiling, …).
  2. @sentry/react root — curated re-exports from @sentry/browser/core only (no export * from the full browser package).
  3. Subpaths for optional React features so they stay off the root export list:
    • @sentry/react/tanstackrouter
    • @sentry/react/redux
    • @sentry/react/reactrouter, reactrouterv3v7, reactrouter.compat
    • @sentry/react/optional-browser-api (Replay, Feedback, AI, …)
  4. @sentry/nextjs / @sentry/gatsby re-export optional-browser-api so import * as Sentry from '@sentry/nextjs' keeps the historical surface.

Migration

Optional / router APIs are no longer available from the @sentry/react root:

// Before
import { replayIntegration, tanstackRouterBrowserTracingIntegration } from '@sentry/react';

// After
import { replayIntegration } from '@sentry/browser'; // or @sentry/replay
import { tanstackRouterBrowserTracingIntegration } from '@sentry/react/tanstackrouter';

Recommended pattern for deferred tracing without reintroducing the fat namespace:

import { init, addIntegration } from '@sentry/react';
import { tanstackRouterBrowserTracingIntegration } from '@sentry/react/tanstackrouter';

init({ dsn: '...' });

requestIdleCallback(() => {
  addIntegration(tanstackRouterBrowserTracingIntegration(router));
});

Test plan

  • packages/react export-surface unit test (exports.treeshake.test.ts)
  • yarn build:dev:filter @sentry/react succeeds
  • CI: browser/react/nextjs/gatsby unit tests
  • Confirm size-limit / package publish layout for new subpath entries

Note

Medium Risk
Public API break for anyone importing optional/router symbols from @sentry/react root; framework re-exports limit impact for Next/Gatsby/Remix users but direct React apps need migration.

Overview
Makes @sentry/react tree-shakeable by default so dynamic import('@sentry/react') (especially with Rolldown/Vite 8 namespace destructuring) no longer drags Replay, Feedback, AI instrumenters, and router/Redux code into the main chunk.

The root entry stops export * from '@sentry/browser' and instead re-exports a curated surface from new @sentry/browser/core. Optional browser APIs move to @sentry/react/optional-browser-api; React Router, Redux, and TanStack helpers move to package subpaths (/tanstackrouter, /redux, /reactrouterv6, etc.) with separate Rollup entrypoints and package.json exports.

Framework SDKs (@sentry/nextjs, @sentry/gatsby, @sentry/remix, @sentry/tanstackstart-react) re-export optional-browser-api (and Redux/TanStack where needed) so import * as Sentry from '@sentry/…' keeps the old symbols for those packages.

Breaking for direct @sentry/react users: replayIntegration, router integrations, createReduxEnhancer, etc. must be imported from @sentry/browser, subpaths, or dedicated packages. CHANGELOG, README, e2e fixtures, and exports.treeshake.test.ts document and enforce the new surface.

Reviewed by Cursor Bugbot for commit fd30d7d. Bugbot is set up for automated code reviews on this repo. Configure here.

Stop star-exporting the full @sentry/browser surface from @sentry/react.
Bundlers that materialize a dynamic import() namespace (notably Rolldown when
destructuring the result) were pulling optional features such as Session
Replay, Feedback, AI instrumenters, and unused router integrations into the
critical path even when apps never referenced them.

Add @sentry/browser/core for the slim error-monitoring + tracing surface,
move React router/Redux integrations to package subpaths, and re-export
optional browser APIs via @sentry/react/optional-browser-api for framework
SDKs that need the historical surface.
@anonrig anonrig requested review from a team as code owners July 9, 2026 21:22
@anonrig anonrig requested review from Lms24, chargome, logaretm, mydea and nicohrubec and removed request for a team July 9, 2026 21:22
Comment thread packages/react/src/browser-api.ts
anonrig added 2 commits July 9, 2026 17:29
uiProfiler was part of the public @sentry/browser exports re-exported via
export * from @sentry/react. The curated browser-api list omitted it, which
would break import { uiProfiler } from '@sentry/react' (and framework SDKs
that re-export the React surface).
…eUrl

uiProfiler was still missing from @sentry/react/optional-browser-api while
browserProfilingIntegration was present, so optional-path profiling setups
could not call uiProfiler. Also re-export getAbsoluteUrl, the last remaining
public @sentry/browser value missing from the curated React surfaces.
Comment thread packages/nextjs/src/client/index.ts
Comment thread packages/react/src/index.ts
Comment thread packages/react/src/optional-browser-api.ts
anonrig and others added 2 commits July 9, 2026 17:57
Restore createReduxEnhancer on the Next.js client and Gatsby surfaces so
index.types and historical import * usage keep working, re-export the
FeatureFlagsIntegration type from optional-browser-api, and document the
intentional @sentry/react root export break in the changelog and README.

Co-Authored-By: Grok <noreply@x.ai>
…ypes

Mirror nextjs/gatsby so dual-typed framework entries keep the historical
optional browser surface after @sentry/react stopped star-exporting it.

Also re-export tanstackRouterBrowserTracingIntegration for tanstackstart-react.

Co-Authored-By: Grok <noreply@x.ai>
@anonrig

anonrig commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Automated fix: addressed CI failure in Build (@sentry/remix / @sentry/tanstackstart-react types).

Comment thread packages/nextjs/src/client/index.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cf92bd3. Configure here.

Comment thread packages/remix/src/client/index.ts
anonrig and others added 2 commits July 9, 2026 18:23
Explicitly re-export uiProfiler after dual export * from @sentry/react and
optional-browser-api so ESM does not omit the conflicting name. Re-export
createReduxEnhancer from remix client/cloudflare to match nextjs/gatsby.

Co-Authored-By: Grok <noreply@x.ai>
Import replay from optional-browser-api and router helpers from package
subpaths (reactrouterv6/v7/reactrouter/reactrouter.compat/tanstackrouter)
instead of the @sentry/react root, which no longer re-exports them.

Co-Authored-By: Grok <noreply@x.ai>
@anonrig

anonrig commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Automated fix: updated E2E apps for subpath/optional-browser-api imports.

react-router-7-spa-streaming still called Sentry.spanStreamingIntegration
after optional APIs left the @sentry/react root.

Co-Authored-By: Grok <noreply@x.ai>
@Lms24

Lms24 commented Jul 10, 2026

Copy link
Copy Markdown
Member

Hey @anonrig thanks for opening this PR! I understand the issue at hand and can see why the barrel-export style causes an issue with Rolldown at the moment (more on that below).

If we assume that bundlers are not smart enough for tree shaking dynamic imports, then I agree that the current exports of the top level Sentry packages are far from ideal. However, we have to weigh the proposed changes against a major usability trade-off: The top-level export is convenient. Users don't need to look up which subpath export contains what. It's just there. Moreover, it ensures usage parity between our NPM package and CDN bundles, something that has always been important for our users. If we were to change this, we'd need to consider that it would be a breaking change, affecting the majority of our users.

Ease of use of our SDKs is a priority for us. Bundle size, too. The Sentry JS SDKs are therefore designed to fully leverage tree shaking. We even provide advanced tree shaking options for users who are really bundle-size conscious.

If you use the SDK as documented, without dynamic imports, every modern bundler (basically everything after Webpack 4 and esbuild) is able to tree-shake. We don't document dynamically importing the SDK because in all our testing so far, it had no noticeable advantage over a classic eager import. Especially for an integration like BrowserTracing (i.e. tanstackRouterBrowserTracingIntegration in your case), it's essential that it is initialized as early as possible in the app lifecycle. Otherwise, there's risk of losing data. While the dynamic import is of course possible, there will be trade-offs. For example, network requests going out before the tracing integration is fully initialized will not carry Sentry's tracing headers, meaning, distributed tracing won't work for these requests.

Since you're using Rolldown and it indeed currently can't tree-shake dynamic imports, @logaretm took a stab at fixing this bug directly in Rolldown: rolldown/rolldown#10213. Once this lands, there should be noticeably better tree shaking for dynamically imported modules.

Our own overhead tests show little to no advantage when comparing an eagerly imported SDK setup with tracing to dynamically importing tracing:

(Actually throttled slow 4g Network)
image

(Lighthouse-Lantern simulated slow 4g speeds)
image

For specific integrations we provide Sentry.lazyLoadIntegration, which will lazily load a CDN bundle that only contains integration-specific code. It's a safe alternative for things like user feedback, where timing isn't critical.

The PR proposes to limit the top-level export surface to a set of "core" APIs. While I fully understand why this is proposed, I think defining this "core" set so that everyone is happy is close to impossible and we'd have to draw an arbitrary line. More importantly, assuming bundlers cannot tree shake dynamically imported code, you'd still end up with a considerable amount of unused code in the core part.

We're not dismissing the idea of lazy-loading parts of the SDK. In fact, it's on our to-do list to pick up after the next major version of the SDK lands. However, as we've seen, it requires careful consideration. I'm therefore closing this PR, since it is not a direction we'll take and we can't merge a breaking change at this time. Again, thank you for opening the PR!

@Lms24 Lms24 closed this Jul 10, 2026
@Lms24 Lms24 self-assigned this Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants