Repository navigation
perf(router): warm prefetched navigations by decoding eagerly - #554
Merged
Merged
Conversation
Prefetch now decodes the RSC stream up front (createFromFetch) instead of on
click. Parsing the Flight stream resolves the route's client references, so its
JS chunk is imported during the prefetch; the cached decoded payload is reused
on navigation and the click loads no new code. Applies to all prefetch
strategies. Ports waku #2099 / #2100.
The prefetch cache now stores a DecodedPrefetch { payload, streamComplete,
scope } instead of a raw Response; navigation warm-hits reuse the payload and
skip createFromFetch. No server-action store-rebinding is needed -- rango
dispatches actions through the global __viteRscCallServer (set once via
setServerCallback, read live at invocation), so nothing navigation-specific is
baked into the decoded tree -- and rango has no per-route preload table to
misalign, so waku's two fixes collapse to just the eager decode here.
Speculative prefetches carrying a control header (X-RSC-Reload/Redirect) are
dropped rather than warmed, so a prefetch never reloads or throws; navigation
re-fetches and acts on the header. teeWithCompletion gains a silent flag so
benign speculative-stream errors don't log.
commit: |
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
A consumer who adds
prefetchto a<Link>expects a clicked-while-visible route to be warm — instant, no loading work. Today it is only half-warm. The prefetch fetches and buffers the route's RSC bytes, but it does not decode them. Decoding the Flight stream is what resolves the route's client references and imports its JS chunk — and that was deferred to click time. So in DevTools the consumer sees the route'sapp-*.jschunk download on click, not on hover/scroll, and the navigation stalls behind a code fetch + RSC decode that the prefetch was supposed to have already paid for.This is the same gap waku hit in #2099 / #2100. This PR closes it for rango: prefetch now decodes eagerly, so the route's chunk is imported during the prefetch and the click loads no new code.
Before vs. after
The consumer writes the same code — only the runtime behavior changes:
/dashboard?_rsc_partial→ buffer bytesapp-dashboard-*.jsnowObservable in DevTools Network: the route's
.jschunk now appears on hover/scroll/render, and no new JS request fires on click.End-to-end example
Nothing to adopt — warming is automatic for every prefetch strategy (
hover,viewport,render,adaptive). A realistic product nav:Semantics
The prefetch decode happens up front; navigation reuses the result. Each prefetch outcome:
scopefromX-RSC-Prefetch-Scope)createFromFetch, no networkscope: "source"X-RSC-Reload/X-RSC-Redirect!response.ok/ aborted / decode errornullstaleRevalidation/hmr/ intercept navA speculative prefetch never reloads the page or throws a redirect — only a real navigation acts on control headers.
Code changes
browser/prefetch/fetch.ts—executePrefetchFetchdecodes the response with the injectedcreateFromFetch(newsetPrefetchDecoder, wired once inrsc-router.tsxinit likesetServerCallback). Tees off a silent completion tracker; drops control-header responses; stores aDecodedPrefetch.browser/prefetch/cache.ts— entries are nowDecodedPrefetch { payload, streamComplete, scope }instead of a rawResponse. Decoded payloads are reusable (safer than a single-use stream body); TTL/FIFO/generation/inflight-alias machinery unchanged.browser/navigation-client.ts— warm hits reuseentry.payload/entry.streamCompleteand skipcreateFromFetch; cross-source discard keys offentry.scope. Fresh path (control-header validation + decode) is unchanged.browser/response-adapter.ts—teeWithCompletion(..., silent): prefetch suppresses benign speculative-stream-error logs; the fresh-navigation path still logs.Why no waku-style store-rebinding: rango dispatches server actions through the global
__viteRscCallServer(set once viasetServerCallback, read live at invocation), and the nav/prefetch decode passes no per-callcallServer— so nothing navigation-specific is baked into the decoded tree. Why waku's issue #1 (wrong-chunk preload table) is N/A: rango resolves chunks by the reference id embedded in the payload (virtual:vite-rsc/client-references), not a per-route index table. Both waku fixes collapse to the single eager-decode change here.Test plan
pnpm test:unit) — 3387 passed. New:prefetch-fetchasserts the decode runs exactly once at prefetch and control-header responses are not warmed;navigation-clientasserts warm hits skipcreateFromFetch+ the source-scoped-wildcard discard;prefetch-cacheupdated to the decoded-entry contract.minitestclientChunks: prefetch warms the route's client chunk before the click(nameless/warmroute, render-prefetch to/widgets): asserts the/widget/resource loads during the prefetch and no new.jsloads on click, in both modes. Existinglink-behavior+prefetch-cache-controlgreen dev + prod.route-types-hmr, cloudflarehmr.test.ts), unrelated to this change and confirmed passing in isolation.Notes
viewport— visible links now also import their route chunk, not just buffer bytes. This is intentional (matches waku) and bounded by the existing 5-min TTL + 50-entry FIFO cap.setPrefetchDecoder/DecodedPrefetchare internal (browser runtime), not public API — no export-surface change.