Repository navigation
perf(router): render/caching pipeline optimizations - #658
Merged
Merged
Conversation
commit: |
Server request path:
- classifyRequest resolves the route under the flag the eventual render
uses (isSSR:true for full-render mode), and createMatchContextForFull
reuses the classified snapshot like the partial path already did —
document requests no longer resolve the route twice
- loadManifest cache hits alias the cached Map instead of copying it
entry-by-entry; the fresh path reassigns instead of clear() so a prior
alias can never poison the module cache
- match/middleware contexts reuse the request context's clean URL
instead of re-parsing request.url (~6x per request + once per
middleware before)
- buildMatchResult fuses its ~6 O(n) segment passes
Cache layer:
- "use cache": in-flight dedup for concurrent misses — one leader runs
the function, followers deserialize their own copy of the serialized
envelope (miss stampede after invalidation eliminated)
- "use cache": JSON-safe key args skip the Flight reply encoder via a
deterministic stable-stringify under a disjoint :j: key namespace
(JSON-safe-arg entries cold-start once after upgrade)
- MemorySegmentCacheStore: per-family maxEntries FIFO cap (default
1000) with tag-index cleanup — no more unbounded growth
- VercelCacheStore: envelopes serialize exactly once (size check +
cache.set share one string; reads accept legacy objects), and stale
reads write a tiny {key}:lock companion instead of rewriting the
whole payload
- document-cache mutates the status header in place (clone only for
immutable headers); streamToString joins chunks instead of O(n^2)
string concat
Client pipeline:
- debug template literals (joins/maps/spreads) guarded behind the flag
instead of built eagerly on every production navigation
- cloneHandleData shares immutable bucket arrays (containers still
cloned); processHandles folds the ownership probe + write into one
historyCache scan per streamed yield
- prefetch cache expiry sweep gated behind a soonest-expiry watermark
- event-controller getState/getActionState return memoized snapshots
with stable references; empty inflight list shared
- Link caches the (hover: none) MediaQueryList; rango-state cookie
parsed once per navigation/prefetch operation
ivogt
force-pushed
the
perf/render-cache-pipeline
branch
from
July 4, 2026 13:34
359ff9b to
a3d244a
Compare
ivogt
added a commit
that referenced
this pull request
Jul 4, 2026
…, dev prerender memoization (#662); map loader-snapshot.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Three independent audits of the render/caching pipeline (server request path, cache layer, client navigation) surfaced redundant hot-path work. The most consumer-observable costs: every document request resolved the route twice (classification + match), every
"use cache"call — including fresh hits — paid the React Flight reply encoder just to build its key, N concurrent misses on one key after a tag invalidation each re-ran the cached function and re-wrote the store, the Vercel store serialized every large envelope twice per write and rewrote the whole payload on every stale read, and every production client navigation eagerly built O(segments) debug strings that were thrown away unread.What a consumer sees differently
Nothing to adopt — no API changes are required. Observable effects:
"use cache"under concurrent traffic: a cold or just-invalidated popular key executes once, not N times.MemorySegmentCacheStoreno longer grows without bound: newmaxEntriesoption, default 1000 per family (segments / responses / items / shells), FIFO eviction with tag-index cleanup.End-to-end example
Before: every call — hit or miss — ran
encodeReply([ctxKeyParts, id])to build the key; 20 concurrent first requests for product42randb.products.find20 times and wrote the store 20 times. After: the key is a stable JSON string (use-cache:<id>:j:[...]), one leader runs the query, 19 followers deserialize the leader's serialized result into their own responses, and the store is written once.Semantics table
loading(c, { ssr: false })"use cache"key, JSON-safe args:j:namespace; existing entries for JSON-safe args cold-start once"use cache"key, non-JSON-safe args"use cache"misses, same key{key}:lockwritten; only the claiming reader getsshouldRevalidate: truex-document-cache-statusheaderCode changes
45 files, +2563/−364. Server:
request-classification.ts(resolve under the render's isSSR flag),match-api.ts(full-path snapshot reuse mirroring the partial path),manifest.ts(alias cached Map; reassign-not-clear safety pin),middleware.ts/match-handlers.ts(clean URL from request context),match-result.ts(fused passes). Cache:cache-runtime.ts(in-flightMap<key, Promise<CacheEnvelope>>+jsonSafeKeyPartfast key),memory-segment-store.ts(maxEntries),vercel-cache-store.ts(single-serialize +claimRevalidationlock key),document-cache.ts,segment-codec.ts. Client: debug-flag guards acrosssegment-reconciler/partial-update/etc.,navigation-store.ts(container-onlycloneHandleData+ owner-guarded single-scan write),prefetch/cache.ts(expiry watermark),event-controller.ts(memoized referentially-stable snapshots),Link.tsx,navigation-client.ts/prefetch/fetch.ts(single cookie parse).Deliberately deferred (design passes needed, tracked separately): Flight-string hit splicing (serve cached segments without deserialize→reserialize),
renderSegmentsper-segment element reuse,temporalLazyRefsstreaming.Test plan
pnpm run test:unit:all) green. New suites:match-api-classified-reuse(snapshot reuse/fallback matrix),manifest-cachepoisoning regression (proven red-before-green against in-placeclear()),cache-runtime-inflight(leader/follower/rejection/tags+handles),cache-runtime-fastkey(determinism, fallback, tainted-ctx keys),memory-store-eviction,segment-codec(multibyte split), Vercel store legacy-envelope + lock tests,clone-handle-data-cow,prefetch-cache-watermark,event-controller-snapshot, rango-state read-once tests.middleware.tstouched).--deny-warnings, oxfmt, e2e-bucketing,.gen.tsclean.blog-cache/cache-tagreadx-document-cache-statusin production;use-cache*,cache*, prefetch and handle-streaming suites cover the touched paths).Notes
"use cache"key-namespace change means one cold miss per JSON-safe-args entry on first deploy; TTL windows make this a non-event, but flagging it.maxEntriesdefault (1000) is the only default-behavior change; deployments intentionally holding more per family should raise it.