Skip to content

fix(react): Match React Router navigations against root routes only - #25336

Open
JoshuaKGoldberg wants to merge 4 commits into
developfrom
joshgoldberg/react-router-match-root-routes
Open

JoshuaKGoldberg wants to merge 4 commits into
developfrom
joshgoldberg/react-router-match-root-routes

Conversation

@JoshuaKGoldberg

@JoshuaKGoldberg JoshuaKGoldberg commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

The combination of:

  • allRoutes: holding every nested route alongside its ancestors
  • matchRoutes: flattening each entry's children again

...results in the React Router integration matching against far more branches than the router itself on every navigation. That includes query-only changes (~90ms per interaction on sentry.io).

Fix: matching only root routes cuts most of that cost and names navigations after the route the router actually renders, instead of sometimes picking a nested route matched on its own.

Measured on sentry.io's route tree (1,449 routes, 13 levels deep) Before After Delta
Branches matchRoutes flattens per call 11,528 1,449 −87%
Navigation naming, /explore/metrics/ 50.7ms 9.8ms −81%
Navigation naming, /issues/:groupId/events/ 50.8ms 9.4ms −81%
Navigation naming, /settings/projects/:projectId/keys/ 28.6ms 5.5ms −81%
Navigation naming, /explore/traces/trace/:traceSlug/ 29.7ms 6.9ms −77%
handleNavigation per click in the app (dev build, Chrome trace) 58.4ms 14.5ms −75%
Repro using the SDK, under Node with jsdom
// npm i @sentry/react@11.7.0 react react-dom react-router@6 global-jsdom jsdom && node repro.mjs
// To compare, build this branch (`yarn build && npm pack` in packages/react), `npm i ./sentry-react-11.7.0.tgz`, and rerun.
import 'global-jsdom/register';

for (const method of ['addEventListener', 'removeEventListener', 'dispatchEvent']) {
  globalThis[method] = window[method].bind(window);
}

const Sentry = await import('@sentry/react');
const { useEffect } = await import('react');
const { createMemoryRouter, createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } = await import('react-router');

Sentry.init({
  dsn: 'https://public@example.com/1',
  tracesSampleRate: 1,
  transport: () => ({ send: async () => ({}), flush: async () => true }),
  integrations: [
    Sentry.reactRouterV6BrowserTracingIntegration({ useEffect, useLocation, useNavigationType, createRoutesFromChildren, matchRoutes }),
  ],
});

const spanNames = [];
Sentry.getClient().on('spanStart', span => {
  const { name, attributes } = Sentry.spanToJSON(span);
  if (attributes['sentry.op'] === 'navigation') spanNames.push(name);
});

// ~1,000 routes nested 10 deep, plus a dynamic `:orgId` route
const tree = (depth, prefix) =>
  depth === 0 ? [] : [0, 1].map(i => ({ path: `${prefix}${i}`, children: tree(depth - 1, `${prefix}${i}-`) }));
const routes = () => [{ path: '/', children: [...tree(9, 's'), { path: ':orgId' }] }];

const timeNavigations = async router => {
  const start = performance.now();
  for (let i = 0; i < 50; i++) await router.navigate(`/s1/s1-1/s1-1-1/?q=${i}`);
  return (performance.now() - start) / 50;
};

const withoutSdk = await timeNavigations(createMemoryRouter(routes()));
const sentryRouter = Sentry.wrapCreateMemoryRouterV6(createMemoryRouter)(routes());
const withSdk = await timeNavigations(sentryRouter);
console.log(`SDK time per query-only navigation: ${(withSdk - withoutSdk).toFixed(1)}ms`);

await sentryRouter.navigate('/s0-0');
console.log(`Router renders ${sentryRouter.state.matches.at(-1).route.path}, navigation span is named ${spanNames.at(-1)}`);

// @sentry/react 11.7.0:
//   SDK time per query-only navigation: ~20ms
//   Router renders :orgId, navigation span is named /s0-0
// This PR:
//   SDK time per query-only navigation: ~5ms
//   Router renders :orgId, navigation span is named /:orgId

Fixes #25340.

@logaretm logaretm left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice find! Just one question on a possible opportunity.

Comment thread packages/react/src/reactrouter-compat-utils/instrumentation.tsx
@linear-code

linear-code Bot commented Oct 13, 2026

Copy link
Copy Markdown

JS-3932

@JoshuaKGoldberg
JoshuaKGoldberg marked this pull request as ready for review October 13, 2026 19:05
@JoshuaKGoldberg
JoshuaKGoldberg requested a review from a team as a code owner October 13, 2026 19:05
@JoshuaKGoldberg
JoshuaKGoldberg requested review from nicohrubec and s1gr1d and removed request for a team October 13, 2026 19:05

This branch has not been deployed

No deployments
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.

React Router integration matches far more routes than the router on every navigation

2 participants