perf(app-router): cache route-handler dispatch import#2501
perf(app-router): cache route-handler dispatch import#2501NathanDrake2406 wants to merge 1 commit into
Conversation
Route handler requests currently await the generated RSC entry's dynamic import of the dispatch runtime on every request, even after the module has loaded. That keeps an import/promise boundary in the steady-state App Route path. Cache the resolved dispatch function after the first import and call it directly on later route-handler dispatches while keeping the dispatch runtime in its lazy chunk. This preserves page and server-action deferral and avoids the bundle-size regression from a static import.
commit: |
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
|
@TheAlexLichter if you don't mind me asking a noob question, PRs like this where the Ws are quite small aren't really worth it right? I've been trying to optimise the warm SSR path so Vinext performs better on Node (not really a priority, I just wanted to see if we can do better than Next here) but I can't really find anything.
|
|
It depends! As we dont bench all different "types of apps"/cases, as long as you can justify the change that will improve a certain type of apps this should be fine. |

Overview
dispatchAppRouteHandlerfunction after the generated RSC entry's first lazy import.packages/vinext/src/entries/app-rsc-entry.ts,tests/entry-templates.test.ts/api/hellomedian avg2.5881ms->2.4354ms(-0.1527ms/request, about-5.9%); p90 avg3.4362ms->2.9384ms.180,972bytes (52,281gzip) and route dispatch remains a13,716byte split chunk.Why This Is Still A Win
Route handlers are steady-state Worker hot-path requests. The first-principles target is simple: keep cold or optional route-handler runtime out of the main Worker entry, but do not keep paying route-handler module-resolution work after the isolate already loaded that module.
app-route-handler-dispatch; the cache only applies after that.app-route-handler-dispatch; this only changes how the generated entry reaches it after first load.This matters even though the median delta is small. The static-import experiment made the route path faster, but it moved dispatch code into the main entry (
222.63 kB,64.90 kBgzip), which makes unrelated Worker traffic pay for route-handler runtime. This version keeps the main entry at180,972bytes (52,281gzip) and removes repeat steady-state overhead only for isolates that actually serve route handlers.Worker Benchmark
Same fixture and local Worker runtime as the investigation path:
tests/fixtures/cf-app-basic, Wrangler local workerd,/api/hello, 300 warmup requests, 2000 measured requests, 4 rounds.2.5881ms2.4354ms-0.1527ms/request(-5.9%)3.4362ms2.9384ms-0.4978ms/request2.5727,2.4550,2.6480,2.67662.5379,2.3263,2.4365,2.44073.9219,3.0033,3.4633,3.35623.2933,2.7144,2.9570,2.7887Bundle shape after the change:
dist/server/index.js180,972bytesdist/server/index.js52,281bytes13,716bytesWhat Changed
app-route-handler-dispatch.app-route-handler-dispatch.Maintainer review path
packages/vinext/src/entries/app-rsc-entry.tsimport()and only caches the resolved function.tests/entry-templates.test.tsdispatchAppRouteHandlerimport is emitted.Validation
Checks run:
Worker benchmark:
cd tests/fixtures/cf-app-basic ./node_modules/.bin/wrangler dev --config dist/server/wrangler.json --port 4192 --log-level errorThen
/api/hello, 300 warmup, 2000 measured requests, 4 rounds.Risk / compatibility
Non-goals