You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
|`CachedLayout`| Caches the document frame, navigation, and its render timestamp. Its cached bytes contain temporary-reference placeholders rather than concrete `children`, so each invocation can supply fresh values through those slots. |
35
-
|`CachedAsyncContent`| Waits 100 ms on a cache miss. The warmup pass tracks that fill, materializes its Flight result, and reuses it in later renders.|
35
+
|`CachedAsyncContent`| Waits 100 ms on a cache miss. The RSC prerender tracks that fill, materializes its Flight result, and reuses it in later renders. |
36
36
|`DynamicContent`| Calls `markDynamic()`. It returns a pending promise during prerender and `undefined` during a request render, so the request continues immediately into its request-time work. |
37
37
38
38
`createCachedComponent` gives this example the semantics of a small `"use cache"` runtime without requiring a compiler transform. Its argument and result serialization follows [`examples/basic/src/framework/use-cache-runtime.tsx`](../basic/src/framework/use-cache-runtime.tsx). Temporary references are important for `CachedLayout`: its cache key and bytes retain reference markers while the matching reference set supplies the concrete dynamic `children` on each invocation.
@@ -52,7 +52,7 @@ dynamic boundary reached
52
52
-> ready -> abort and retain partial RSC output
53
53
```
54
54
55
-
The two branches are both necessary. A fully static render does not call `markDynamic()`, so its `ready` promise remains pending and `result` wins naturally. A partial render may be cut off only after `dynamicReached` proves that its suspension is intentional and `pendingWork` proves that no tracked cache fill can add more static output.
55
+
Both branches of this race are necessary. A fully static render does not call `markDynamic()`, so its `ready` promise remains pending and `result` wins naturally. A partial render may be cut off only after `dynamicReached` proves that its suspension is intentional and `pendingWork` proves that no tracked cache fill can add more static output.
56
56
57
57
[`src/framework/prerender-context.ts`](./src/framework/prerender-context.ts) implements this compact model with `AsyncLocalStorage`, a set of cache promises, and one `setTimeout(0)` retry window. Production frameworks make the same kind of framework-defined approximation with more complete cache, module, and scheduler tracking.
58
58
@@ -82,19 +82,32 @@ Applications should use the `@vitejs/plugin-rsc` exports rather than importing i
82
82
83
83
## Build flow
84
84
85
-
Each static path goes through three render steps:
85
+
This example sends each static path through three render steps:
86
86
87
-
1.**Warm the RSC cache.**`@vitejs/plugin-rsc/rsc/static``prerender()` discovers cache misses and waits for their useful static work to finish. Its output is discarded because it records the process of filling the cache rather than the final warm-cache shell.
87
+
1.**Warm the RSC cache.**`@vitejs/plugin-rsc/rsc/static``prerender()` discovers cache misses and waits for their useful static work to finish. The demo intentionally discards this result to keep cache discovery and shell capture as separate stages.
88
88
2.**Capture partial Flight.** The same RSC `prerender()` renders a clean pass against the warm cache. Cached content is immediately available, while `DynamicContent` remains pending at `markDynamic()`. Cutting off here produces the static Flight prefix around that intentional hole.
89
89
3.**Capture resumable HTML.**`react-dom/static.edge``prerender()` consumes the decoded partial Flight tree. `preventStreamClose` keeps the missing segment pending, so React DOM emits an HTML `prelude` for the shell and serializable `postponed` state describing where request-time rendering must continue.
90
90
91
+
The restart is illustrative rather than required by this compact runtime. Its readiness gate waits for every tracked cache fill to settle and gives React a retry turn before cutoff, so the first RSC prerender already returns a valid partial Flight prelude that could be passed directly to React DOM. Keeping a second pass makes cache discovery and final shell capture explicit and mirrors production architectures where those phases have different contracts.
92
+
93
+
A useful observation falls out of comparing real frameworks. The specific API that generates each RSC stream does not actually matter, because neither the cache-discovery pass nor the final Flight fed to SSR is bound to the static `prerender()` prelude rather than a plain `renderToReadableStream()`. What matters is that whichever call generates the stream is wrapped in precise control over readiness, staging, and cutoff. Next.js and vinext even reach for the two APIs in opposite arrangements. Next.js lets an initial `prerender()` prelude drive a prospective React DOM render, while its final RSC pass runs `renderToReadableStream()` under framework-controlled staging and cutoff. vinext does the reverse, warming its cache by draining an ordinary `renderToReadableStream()`, then producing its final Flight with the static `prerender()` prelude.
94
+
95
+
So separate phases are demanded by that control layer rather than by the API or by pending cache fills. Cache discovery runs without the tracking, metadata, staging, and cutoff that the authoritative artifact requires, so it cannot double as the final pass. Separate phases are not required merely because cache fills were initially pending.
-[Next.js initial RSC prelude drives prospective React DOM work](https://github.com/vercel/next.js/blob/153bf8ac5fa00888ef5fbb2b65cac12f0942a44f/packages/next/src/server/app-render/app-render.tsx#L8056-L8147)
99
+
-[Next.js final RSC staging and React DOM prerender](https://github.com/vercel/next.js/blob/153bf8ac5fa00888ef5fbb2b65cac12f0942a44f/packages/next/src/server/app-render/app-render.tsx#L8230-L8607)
100
+
-[vinext warmup stream drain and abort](https://github.com/cloudflare/vinext/blob/fd1cc3d3ddaaec8c130d5e4bcae3a6f761089756/packages/vinext/src/server/app-ppr-fallback-shell-render.ts#L28-L55)
101
+
-[vinext final RSC prelude](https://github.com/cloudflare/vinext/blob/fd1cc3d3ddaaec8c130d5e4bcae3a6f761089756/packages/vinext/src/server/app-page-render.ts#L731-L752)
102
+
-[vinext final React DOM prelude](https://github.com/cloudflare/vinext/blob/fd1cc3d3ddaaec8c130d5e4bcae3a6f761089756/packages/vinext/src/server/app-ssr-entry.ts#L615-L624)
103
+
91
104
The build persists the shared RSC cache and each route's `{ prelude, postponed }` result. Repeating the RSC render is safe because React rendering must already be pure and restartable; the cache ensures expensive static work is performed on the miss and replayed by the final pass.
92
105
93
106
The demo intentionally requires a dynamic HTML hole so every generated route exercises `resume`. A complete framework would also persist and serve fully static results and other valid prerender outcomes.
94
107
95
108
### Development flow
96
109
97
-
Development runs the same warmup, final RSC prerender, and React DOM prerender on demand for each document request. It passes the live `{ prelude, postponed }` result directly into request-time resume instead of loading persisted build output.
110
+
Development runs the same chosen warmup, final RSC prerender, and React DOM prerender pipeline on demand for each document request. It passes the live `{ prelude, postponed }` result directly into request-time resume instead of loading persisted build output.
98
111
99
112
Adding `?__ppr` exercises the persistence boundary without a production build. The dev server prerenders all static paths, serializes the shared cache and route results, then revives the selected route before serving it. The rendering model stays the same; only the handoff changes from live objects to their persisted representation.
100
113
@@ -153,7 +166,7 @@ These are deliberate runtime boundaries, not attempts to detect arbitrary applic
153
166
154
167
## Prior art
155
168
156
-
-[Next.js cache warmup and final prerender](https://github.com/vercel/next.js/blob/153bf8ac5fa00888ef5fbb2b65cac12f0942a44f/packages/next/src/server/app-render/app-render.tsx#L7905-L8490)
169
+
-[Next.js prospective and final prerender implementation](https://github.com/vercel/next.js/blob/153bf8ac5fa00888ef5fbb2b65cac12f0942a44f/packages/next/src/server/app-render/app-render.tsx#L7905-L8490)
0 commit comments