|
| 1 | +# Patches |
| 2 | + |
| 3 | +This directory holds [pnpm patches](https://pnpm.io/cli/patch) applied on install via |
| 4 | +`pnpm.patchedDependencies` in the root `package.json`. Each `.patch` is a diff against the |
| 5 | +published package. Most are small and self-explanatory from the diff; the non-obvious ones |
| 6 | +are documented below. |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## `@remix-run/router@1.23.2` — route-matching memoization |
| 11 | + |
| 12 | +**File:** `patches/@remix-run__router@1.23.2.patch` (patches `dist/router.cjs.js`) |
| 13 | + |
| 14 | +### What it does |
| 15 | + |
| 16 | +Three changes to `matchRoutesImpl` / `compilePath`, all pure memoization of work that |
| 17 | +depends only on the **static** route manifest: |
| 18 | + |
| 19 | +1. **Cache flattened + ranked branches per route-tree** (`WeakMap` keyed by the `routes` |
| 20 | + ref). `flattenRoutes()` + `rankRouteBranches()` were recomputed on *every* `matchRoutes` |
| 21 | + call across all ~436 webapp routes. |
| 22 | +2. **Hoist `decodePath(pathname)` out of the branch-match loop** — it's loop-invariant but |
| 23 | + was recomputed once per branch. |
| 24 | +3. **Memoize `compilePath` compiled regexes** by `path|caseSensitive|end` (bounded `Map`, |
| 25 | + cap 2000). The matcher RegExp was rebuilt on every `matchPath` call. |
| 26 | + |
| 27 | +### Why |
| 28 | + |
| 29 | +Profiling the realtime runs feed under load (100 concurrent tag feeds, ~425 req/s) found |
| 30 | +**~68% of webapp CPU was spent in react-router's `matchRoutes`** — re-flattening, |
| 31 | +re-ranking, and re-compiling the entire route table on every request. It is **not** a dev |
| 32 | +artifact: there is no `NODE_ENV` gate, and a `NODE_ENV=production` profile was identical |
| 33 | +(67.9% vs 68.3%). The realtime feed's high request rate (each long-poll returns fast and |
| 34 | +immediately re-polls) just amplifies a latent per-request cost that large route tables pay |
| 35 | +everywhere. |
| 36 | + |
| 37 | +Measured on a single instance, same load, before vs after this patch: |
| 38 | + |
| 39 | +| | before | after | |
| 40 | +|---|---|---| |
| 41 | +| active CPU (self-time / window) | 28.3s | 18.5s (**−34%**) | |
| 42 | +| route-matching self-time | 19.2s | 7.5s (**−61%**) | |
| 43 | +| event-loop lag p99 | 322ms | 113ms (**−65%**) | |
| 44 | +| idle headroom | 26% | 52% | |
| 45 | + |
| 46 | +The realtime machinery itself (router/hydrate/serialize/diff) was ~0% — the bottleneck was |
| 47 | +entirely generic Remix request overhead. |
| 48 | + |
| 49 | +### Upstream status (why we patch instead of upgrade) |
| 50 | + |
| 51 | +This is a known, acknowledged inefficiency, and it is **only partially fixed in React |
| 52 | +Router v7** — which we can't adopt without a full Remix 2 → RR7 framework migration. |
| 53 | + |
| 54 | +- [Issue #8653 "Performance issues"](https://github.com/remix-run/react-router/issues/8653) |
| 55 | + reported it (a user with 12k routes, ~67ms per match) and was closed as a dup of the |
| 56 | + route-ranking discussion [remix#4786](https://github.com/remix-run/remix/discussions/4786). |
| 57 | +- [PR #14866 "Optimize route matching performance with caching"](https://github.com/remix-run/react-router/pull/14866) |
| 58 | + implemented *exactly this patch* (hoist `decodePath`, cache `compilePath`, cache |
| 59 | + flatten/rank), claiming **~80% route-matching CPU reduction on a 400+ route app**. It was |
| 60 | + **closed, not merged.** |
| 61 | +- [PR #14967 "perf: cache flattened/ranked route branches"](https://github.com/remix-run/react-router/pull/14967) |
| 62 | + is the partial fix that *did* ship (in v7): it caches only the branches, threaded via a |
| 63 | + `precomputedBranches` param through the framework's server-runtime (~15% SSR gain). It |
| 64 | + does **not** cache `compilePath` — that regex rebuild remains even on `main`. |
| 65 | + ([PR #14971](https://github.com/remix-run/react-router/pull/14971) added client-side wins.) |
| 66 | + |
| 67 | +The maintainer's reasoning for closing the fuller PR (#14866), verbatim: |
| 68 | + |
| 69 | +> "This is great as a `patch-package` optimization for those who want it, but we are |
| 70 | +> actively working on integrating the more performant route-pattern library from Remix 3 so |
| 71 | +> we'd rather just do the right 'fix' and ship the new algorithm instead of trying to |
| 72 | +> band-aide perf improvements to the existing algorithm which was written with a very |
| 73 | +> different set of constraints. Those constraints come from early v6 when it was only |
| 74 | +> declarative mode so route trees were defined at render time and thus had to be |
| 75 | +> re-flattened/re-ranked/re-compiled every time." |
| 76 | +
|
| 77 | +So: the re-compute-everything design is a holdover from early React Router v6 declarative |
| 78 | +mode (route trees defined at render time, so recomputing was correct then). The maintainer |
| 79 | +**explicitly endorsed patch-package as the interim approach** and is betting on the Remix 3 |
| 80 | +route-pattern rewrite for the real fix. This patch is that sanctioned stopgap — and it also |
| 81 | +includes the `compilePath` cache the merged PR left on the table. |
| 82 | + |
| 83 | +### Safety |
| 84 | + |
| 85 | +Pure memoization of deterministic, internal-only values: |
| 86 | + |
| 87 | +- `flattenRoutes`/`rankRouteBranches` and the compiled regexes depend solely on the static |
| 88 | + route manifest; the cached values are never returned to or mutated by the framework. |
| 89 | +- The compiled `RegExp` has no `/g` flag, so `.exec()` carries no cross-call state — safe to |
| 90 | + share under concurrency. |
| 91 | +- The branch cache is a `WeakMap` (collected with its route tree); the compile cache is |
| 92 | + bounded at 2000 entries (route patterns are a static set; the cap only guards any dynamic |
| 93 | + `matchPath()` use). |
| 94 | +- Targets the **CJS** build (`dist/router.cjs.js`), which the webapp server loads at runtime |
| 95 | + (`@remix-run/router` is not bundled into the server build). |
| 96 | + |
| 97 | +### When to remove |
| 98 | + |
| 99 | +Drop this patch if/when the webapp moves to React Router v7+ (which threads |
| 100 | +`precomputedBranches` itself) or the Remix 3 route-pattern matcher lands. Re-profile at that |
| 101 | +point — the `compilePath` cache may still be worth keeping since upstream never added it. |
0 commit comments