Visitar URL original
Optimize route matching performance with caching by punkpeye · Pull Request #14866 · remix-run/react-router · GitHub
Skip to content

Optimize route matching performance with caching - #14866

Closed
punkpeye wants to merge 2 commits into
remix-run:mainfrom
punkpeye:optimize-route-matching
Closed

punkpeye wants to merge 2 commits into
remix-run:mainfrom
punkpeye:optimize-route-matching

Conversation

@punkpeye

Copy link
Copy Markdown

Hoist decodePath() call above the matching loop since pathname is loop-invariant, cache compilePath() RegExp results in a bounded Map (max 1000 entries), and cache flattenRoutes/rankRouteBranches results with version-based invalidation triggered by patchRoutesImpl.

Based on our internal testing (large app with 400+ routes), this reduces CPU consumed by react-router route matching ~80%.

Hoist decodePath() call above the matching loop since pathname is
loop-invariant, cache compilePath() RegExp results in a bounded Map
(max 1000 entries), and cache flattenRoutes/rankRouteBranches results
with version-based invalidation triggered by patchRoutesImpl.

Based on our internal testing (large app with 400+ routes), this reduces
CPU consumed by react-router route matching ~80%.
@changeset-bot

changeset-bot Bot commented Mar 10, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4778f3c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@remix-cla-bot

remix-cla-bot Bot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Hi @punkpeye,

Welcome, and thank you for contributing to React Router!

Before we consider your pull request, we ask that you sign our Contributor License Agreement (CLA). We require this only once.

You may review the CLA and sign it by adding your name to contributors.yml.

Once the CLA is signed, the CLA Signed label will be added to the pull request.

If you have already signed the CLA and received this response in error, or if you have any questions, please contact us at hello@remix.run.

Thanks!

- The Remix team

@remix-cla-bot

remix-cla-bot Bot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳

@punkpeye

Copy link
Copy Markdown
Author

Relative CPU allocation before and after:

  ┌───────────────────┬────────┬───────┬────────────────────────┐
  │     Function      │ Before │ After │         Impact         │
  ├───────────────────┼────────┼───────┼────────────────────────┤
  │ compilePath       │ 19.3%  │ 7.9%  │ -11.4% (cache working) │
  ├───────────────────┼────────┼───────┼────────────────────────┤
  │ rankRouteBranches │ 4.0%   │ 0.0%  │ -4.0% (cached)         │
  ├───────────────────┼────────┼───────┼────────────────────────┤
  │ flattenRoute      │ 4.2%   │ 1.6%  │ -2.6% (cached)         │
  ├───────────────────┼────────┼───────┼────────────────────────┤
  │ computeScore      │ 2.5%   │ 1.0%  │ -1.5% (cached)         │
  ├───────────────────┼────────┼───────┼────────────────────────┤
  │ flattenRoutes     │ 1.9%   │ 0.6%  │ -1.3% (cached)         │
  └───────────────────┴────────┴───────┴────────────────────────┘


return [matcher, params];
if (compilePathCache.size >= COMPILE_PATH_CACHE_MAX) {
compilePathCache.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

compilePathCache.delete(compilePathCache.keys().next().value) to not the drop the cache fully, but preferring FIFO?

@brophdawg11

Copy link
Copy Markdown
Contributor

This is great as a patch-package optimization for those who want it, but we are actively working on integrating the more performant route-pattern library from Remix 3 so we'd rather just do the right "fix" and ship the new algorithm instead of trying to band-aide perf improvements to the existing algorithm which was written with a very different set of constraints. Those constraints come from early v6 when it was only declarative mode so route trees were defined at render time and thus had to be re-flattened/re-ranked/re-compiled every time. The route pattern work is the more efficient version of that for data/framework mode.

ericallam added a commit to triggerdotdev/trigger.dev that referenced this pull request Jun 9, 2026
…m patch (#3877)

## Summary

Under high request load the webapp spends most of its CPU inside
react-router's `matchRoutes`, not in application code.
`@remix-run/router@1.23.2` (the React Router v6 / Remix 2 core)
re-flattens, re-ranks, and recompiles the entire route table on every
request, and with the webapp's ~436 routes that cost dominates once
request rates climb. There is no `NODE_ENV` gate, so production pays it
too.

This adds a pnpm patch that memoizes the parts that depend only on the
static route manifest: it caches the flattened/ranked branches per route
tree, hoists the loop-invariant `decodePath` out of the match loop, and
caches compiled path regexes.

## Benchmark

CPU profile over the same load (100 concurrent tag feeds, ~425 req/s),
`NODE_ENV=production`, before vs after the patch:

| Metric | Before | After |
| --- | --- | --- |
| Active CPU (self-time over the window) | 28.3s | 18.5s (-34%) |
| Route-matching self-time | 19.2s | 7.5s (-61%) |
| Event-loop lag p99 | 322ms | 113ms (-65%) |
| Idle headroom | 26% | 52% |

Application/realtime code was ~0% of CPU in both profiles; the
bottleneck was entirely generic per-request route matching.

## Why a patch instead of an upgrade

The inefficiency is acknowledged upstream
([remix-run/react-router#8653](remix-run/react-router#8653)).
A contributor PR doing exactly this
([remix-run/react-router#14866](remix-run/react-router#14866))
was closed in favor of a narrower fix
([remix-run/react-router#14967](remix-run/react-router#14967),
branch caching only, shipped in React Router v7), with the maintainer
suggesting patch-package as the interim until the Remix 3 route-pattern
rewrite (see
[remix-run/remix#4786](remix-run/remix#4786)).
We are on the v6-era core and cannot pick up even the partial fix
without a framework migration, so this patch is the sanctioned stopgap,
and it also includes the compiled-regex cache the merged PR left out.


[`patches/README.md`](https://github.com/triggerdotdev/trigger.dev/blob/perf/react-router-route-matching/patches/README.md)
documents the full rationale, the safety argument (deterministic,
internal-only, bounded caches), and when to remove the patch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants