Repository navigation
test(ppr): pin the outlined-bake guarantee — ready Suspense content bakes; masked holes postpone (#702) - #703
Merged
Merged
Conversation
…akes into the shell; masked holes postpone (closes #702) Investigation outcome (issue #702): the suspected defect does NOT exist. Control experiments proved the capture already bakes outlined-but-ready content through the real abort path (6.2MB prelude, postponed=null on a bare ppr route; 6.2MB + 1.5KB postponed beside a masked slot hole), and ready fizz work cannot lose the abort race structurally: React's tracked-postpones pings run on scheduleMicrotask while the post-quiesce hops and abort ride setTimeout, so every runnable task drains before the first hop fires. A counted-abort gate would be a no-op for ready work and would BREAK the physics-hole doctrine (parked unmasked I/O must postpone). The original red reproducer was doctrine-inconsistent, not a bug repro: under route-level loading(), LoaderBoundary use()es the masked loader ABOVE the entire route subtree, so the whole body IS the hole — its 30k rows were unreachable at capture (4.5KB prelude), not ready-but-unflushed. React postpones whole Suspense boundaries, so bytes inside a postponed boundary can never ride the prelude. What this PR pins, on the reshaped /shell-cache/outlined fixture (10k-row outlined section BESIDE a masked @outlinedBadge slot hole, never under it): - every outlined row lands in the stored prelude (byte-provenance via splitPrelude, rows 0 and 9999 before the first </html>); - the masked hole still postpones (badge fallback frozen in the prelude, digit-anchored value only in the resumed tail, seq advances per HIT while the prelude stays byte-identical); - dev + production describes both run it. Also documents the microtask-atomicity invariant on the abort hop loop in ssr/index.tsx (comment-only src change) so a work-quiet/counted gate is not re-attempted, and the fixture carries the doctrine clarification: content that must bake belongs beside the hole (slot/layout chrome), not under route-level loading(). Battery: router unit 5052; shell suites 33 dev / 34 production; prerender-ppr 4+4 (producer B composition); cloudflare ppr battery 27 passed (3 pre-existing producer-B dev-boot flakes, untouched files); typecheck/lint/format green.
commit: |
This was referenced Jul 6, 2026
Merged
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
Issue #702 suspected that the PPR capture abort could land before fizz flushed outlined-but-ready Suspense content (any boundary >500 bytes gets fallback-first + outlined treatment in a tracked-postpones prerender), silently postponing content that had zero pending data — suspected as the router-level cause of a measured storefront pathology (~100 postponed boundaries, ~2.7MB re-rendered per HIT).
Finding: the defect does not exist — and the guarantee was unpinned
Control experiments (taps since removed) proved:
postponed=nullon a bare ppr route, and 6.2MB + 1.5KB postponed state beside a masked slot hole.scheduleMicrotask; the post-quiesce hops and abort ridesetTimeout— every runnable task drains before the first hop fires.loading(),LoaderBoundaryuse()es the masked loader above the whole route subtree, so the entire body IS the hole — its rows were unreachable at capture (4.5KB prelude), not ready-but-unflushed.The storefront pathology is the app-level shape (content under Suspense wrappers whose data situation makes them holes), cured by page structure — not by abort policy.
What a consumer sees
No behavior change (the one src edit is a comment documenting the microtask-atomicity invariant so a work-quiet/counted gate is not re-attempted). The consumer-visible contract this PR pins with tests, previously unpinned:
<BigSyncSection/>(any size) bakes into the stored shell — served from the prelude on every HIT, zero per-request render;loading()— underloading(), the whole route body is the hole by design.Semantics table
</html>)loading())loading()Code-change summary
e2e/test-app/src/urls/shell-cache.tsx—/shell-cache/outlinedfixture: 10k-row outlined section +@outlinedBadgemasked slot hole (own loader +loading()),ShellOutlinedChromeLayout; scar-tissue comments on the loading() shape.e2e/test-app/src/urls/shell-cache.defs.ts—ShellOutlinedBadgeLoader(isolated seq counter).e2e/shell-cache.test.ts— the guarantee test (byte-provenance viasplitPrelude; runs in both dev and(production)describes viarunShellCacheSpec).src/ssr/index.tsx— comment-only: the microtask-atomicity invariant on the abort hop loop.router.named-routes.gen.ts— regenerated (shellCacheOutlined).Test plan
test:unit:allgreen across all packages.Notes