Repository navigation
perf(ppr): eager Flight injection on resume — hydration no longer waits for the slowest live loader; FE/server boot debug logs; directoryClientChunks export - #691
Merged
Conversation
…segment tree build, loader awaits, hydration commit The initial document path was silent under INTERNAL_RANGO_DEBUG: FE logs only started at the first soft navigation, so a boot stall (an await holding initBrowserApp, and with it hydrateRoot) was invisible. Adds [Browser][boot] steps through initBrowserApp (flight decode, handle stream consumption with per-push logs, bridge wiring, initial tree) plus a hydration-commit signal from Rango's root effect, and [Browser][segments] steps through renderSegments (per-segment build, blocking vs streaming loader awaits with timings, per-loader settle taps). All gated on the baked flag + typeof window, so prod bundles DCE them and SSR/RSC stay silent.
…ion payload behind the slowest live loader On a shell HIT the stock rsc-html-stream injectRSCPayload starts forwarding Flight chunks only after the first fizz chunk, and the resumed fizz emits nothing until the first hole's loaders resolve. The Flight root row (ready ~30ms into the tail render) therefore rode the response ~1.5s late and the client could not call hydrateRoot until then — hydration start was gated on the slowest live loader instead of on the payload being ready. injectRSCPayloadEager pumps Flight immediately in start(); ordering safety via a single output task queue (same-tick fizz chunks flush as one atomic batch, scripts only land between batches; trailer stripped/re-appended once). Resume + DATA variants only — the normal document path keeps the stock injector (scripts must not precede the doctype there). Also adds INTERNAL_RANGO_DEBUG-gated [Server][ppr] stream-timing logs (prelude enqueue, first flight chunk, first flight script, first resumed HTML batch, first tail chunk) so hydration-start latency is attributable to real server work vs stream plumbing. 7 unit tests (ordering, trailer, escaping, nonce, DATA variant, base64 fallback); shell-cache + shell-secure e2e: 58 passed (dev + production).
…ions can overlay the built-in strategy
A custom clientChunks function replaces the built-in route/marker grouping
entirely; without access to the built-in, overriding a handful of modules
silently degrades the rest of the app to plugin-rsc's default serverChunk
grouping. Exporting the strategy lets consumers delegate:
clientChunks: (meta) =>
meta.normalizedId.includes("/hec/builder/")
? "builder-island"
: directoryClientChunks(meta)
Called without a ClientChunkContext the app-fallback split is inactive
(discovery wires that only for the built-in default).
…ientChunks export
ivogt
force-pushed
the
experiment/fe-boot-debug
branch
from
July 5, 2026 17:01
139f512 to
2df372b
Compare
commit: |
4 tasks done
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 statement
On a PPR shell HIT, hydration could not start until the slowest live loader finished — even though the payload the client needed was ready within milliseconds. A consumer's storefront homepage painted its cached shell at ~70ms, but
hydrateRootwaited 1.5–3.5s:rsc-html-stream'sinjectRSCPayloadonly starts forwarding Flight chunks after the first HTML chunk flows through it, and the resumed fizz render emits nothing until the first hole's loaders (per-request SFCC basket/wishlist calls) resolve. The entire hydration payload — root row included — sat parked in a buffer behind data it did not depend on. Separately, none of this was observable: FE debug logs underINTERNAL_RANGO_DEBUGonly started at the first soft navigation, so the initial-document boot (flight decode → handle stream → tree build → hydrateRoot) was a black box.Before vs. after
Before (consumer app, PPR homepage, warm shell HIT):
After (same page, same loaders):
And the boot is now narrated end to end under
INTERNAL_RANGO_DEBUG=1:End-to-end consumer example
Measured on a real consumer storefront (yarn workspaces,
@cloudflare/vite-plugin, PPR homepage with three no-loading()chrome loaders backed by SFCC APIs), production build served viavite preview:The structural win over the raw numbers: hydration start no longer depends on loader latency at all — a slow basket API can no longer delay interactivity of the shell.
Semantics
start(); fizz batches interleave atomicallyinjectRSCPayloadINTERNAL_RANGO_DEBUGunset / production buildclientChunkscustom functiondirectoryClientChunksis exportedCode changes
src/ssr/inject-rsc-eager.ts(new) —injectRSCPayloadEager: pumps Flight instart(), before any HTML exists. Ordering safety via a single output task queue: same-tick fizz chunks flush as one atomic batch (never split by a script), each Flight script is its own task; trailer stripped from passing HTML and re-appended once after both streams complete. Safe on resume because the stored prelude — a complete document through</body></html>— is already on the wire, so every tail byte is foster-parented.src/ssr/index.tsx—createShellResumeHandlerpipes through the eager injector for both the resume and DATA variants.createSSRHandler(normal path) is untouched.src/rsc/rsc-rendering.ts— flag-gated[Server][ppr]stream timing taps (prelude enqueue, flight first chunk, first tail chunk) inserveShellHit, so this class of stall is attributable from the terminal.src/browser/rsc-router.tsx,src/browser/logging.ts—bootLog()steps throughinitBrowserApp(flight decode, handle-stream consumption with per-push logs, bridge wiring, initial tree) plus a once-only hydration-commit signal fromRango's root effect.src/segment-system.tsx,src/segment-loader-promise.ts—[Browser][segments]steps: per-segment tree build, blocking vs streaming loader awaits with timings, per-loader settle taps. All gated on the baked flag ANDtypeof window, so SSR/RSC renders stay silent and prod bundles drop the code entirely.src/vite/index.ts— exportdirectoryClientChunksso a customclientChunksfunction can overlay the built-in route grouping instead of silently replacing it.docs/internal/feature-file-map.md,docs/internal/feature-map.md— synced.Why the injector lives in the router instead of patching
rsc-html-stream: the lazy start is correct for the normal document path (upstream's only path); eagerness is only valid where a prelude already closed the document — that knowledge is rango's.Test plan
src/ssr/__tests__/inject-rsc-eager.test.ts— 7 tests. The headline regression test ("emits flight scripts while the HTML side is still silent") drives the injector with a stalled HTML source and asserts the first output chunk is a flight script — the stock injector fails this by construction. Also pinned: atomic same-tick HTML batches (split-tag reassembly), single trailing</body></html>,</scriptescaping, nonce on every script tag, chunkless-source (DATA variant) no-deadlock, base64 fallback for invalid-unicode chunks.shell-cache+shell-secure— 58 passed, dev + production (the entire PPR serve/resume surface over the new injector).internal-debug-fe— 4 passed, dev + production (boot logs additive; existing flag-delivery contract intact).semantic-matrix— 45 passed (incl. PPR3 consumption-lane row).pnpm run typecheck,pnpm run test:unit:all(0 failures),pnpm run lint,pnpm run format,pnpm check:docs-api(167 identifiers verified).first flight script +14msvs first tail HTML at +226ms.Notes
directoryClientChunkscalled without aClientChunkContextleaves theapp-fallbacksplit inactive (discovery wires that only for the built-in default) — documented on the export.