Repository navigation
Conversation
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%.
|
|
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 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 |
|
Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳 |
|
Relative CPU allocation before and after: |
|
|
||
| return [matcher, params]; | ||
| if (compilePathCache.size >= COMPILE_PATH_CACHE_MAX) { | ||
| compilePathCache.clear(); |
There was a problem hiding this comment.
compilePathCache.delete(compilePathCache.keys().next().value) to not the drop the cache fully, but preferring FIFO?
|
This is great as a |
…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.
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%.