@@ -168,7 +168,10 @@ import {
168
168
prerenderAndAbortInSequentialTasks ,
169
169
} from './app-render-prerender-utils'
170
170
import { printDebugThrownValueForProspectiveRender } from './prospective-render-utils'
171
- import { scheduleInSequentialTasks } from './app-render-render-utils'
171
+ import {
172
+ pipelineInSequentialTasks ,
173
+ scheduleInSequentialTasks ,
174
+ } from './app-render-render-utils'
172
175
import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler'
173
176
import {
174
177
workUnitAsyncStorage ,
@@ -197,6 +200,7 @@ import {
197
200
trackPendingChunkLoad ,
198
201
trackPendingImport ,
199
202
trackPendingModules ,
203
+ trackPendingModulesInRender ,
200
204
} from './module-loading/track-module-loading.external'
201
205
import { isReactLargeShellError } from './react-large-shell-error'
202
206
import type { GlobalErrorComponent } from '../../client/components/builtin/global-error'
@@ -2143,57 +2147,198 @@ async function renderToStream(
2143
2147
// We only have a Prerender environment for projects opted into cacheComponents
2144
2148
experimental . cacheComponents
2145
2149
) {
2146
- // This is a dynamic render. We don't do dynamic tracking because we're not prerendering
2147
- const RSCPayload : InitialRSCPayload & {
2150
+ type RSCPayloadWithValidation = InitialRSCPayload & {
2148
2151
/** Only available during cacheComponents development builds. Used for logging errors. */
2149
2152
_validation ?: Promise < React . ReactNode >
2150
- } = await workUnitAsyncStorage . run (
2151
- requestStore ,
2152
- getRSCPayload ,
2153
- tree ,
2154
- ctx ,
2155
- res . statusCode === 404
2156
- )
2153
+ }
2154
+
2157
2155
const [ resolveValidation , validationOutlet ] = createValidationOutlet ( )
2158
- RSCPayload . _validation = validationOutlet
2159
2156
2160
- const debugChannel = setReactDebugChannel && createDebugChannel ( )
2157
+ const getPayload = async ( ) : Promise < RSCPayloadWithValidation > => {
2158
+ const payload : RSCPayloadWithValidation =
2159
+ await workUnitAsyncStorage . run (
2160
+ requestStore ,
2161
+ getRSCPayload ,
2162
+ tree ,
2163
+ ctx ,
2164
+ res . statusCode === 404
2165
+ )
2166
+ // Placing the validation outlet in the payload is safe
2167
+ // even if we end up discarding a render and restarting,
2168
+ // because we're not going to wait for the stream to complete,
2169
+ // so leaving the validation unresolved is fine.
2170
+ payload . _validation = validationOutlet
2171
+ return payload
2172
+ }
2161
2173
2162
- if ( debugChannel ) {
2174
+ const setDebugChannelForClientRender = (
2175
+ debugChannel : DebugChannelPair
2176
+ ) => {
2163
2177
const [ readableSsr , readableBrowser ] =
2164
2178
debugChannel . clientSide . readable . tee ( )
2165
2179
2166
2180
reactDebugStream = readableSsr
2167
2181
2168
- setReactDebugChannel (
2182
+ setReactDebugChannel ! (
2169
2183
{ readable : readableBrowser } ,
2170
2184
htmlRequestId ,
2171
2185
requestId
2172
2186
)
2173
2187
}
2174
2188
2175
- const reactServerStream = await workUnitAsyncStorage . run (
2189
+ const environmentName = ( ) =>
2190
+ requestStore . prerenderPhase === true ? 'Prerender' : 'Server'
2191
+
2192
+ // Try to render the page and see if there's any cache misses.
2193
+ // If there are, wait for caches to finish and restart the render.
2194
+
2195
+ // This render might end up being used as a prospective render (if there's cache misses),
2196
+ // so we need to set it up for filling caches.
2197
+ const cacheSignal = new CacheSignal ( )
2198
+
2199
+ // If we encounter async modules that delay rendering, we'll also need to restart.
2200
+ // TODO(restart-on-cache-miss): technically, we only need to wait for pending *server* modules here,
2201
+ // but `trackPendingModules` doesn't distinguish between client and server.
2202
+ trackPendingModulesInRender ( cacheSignal )
2203
+
2204
+ const prerenderResumeDataCache = createPrerenderResumeDataCache ( )
2205
+
2206
+ requestStore . prerenderResumeDataCache = prerenderResumeDataCache
2207
+ // `getRenderResumeDataCache` will fall back to using `prerenderResumeDataCache` as `renderResumeDataCache`,
2208
+ // so not having a resume data cache won't break any expectations in case we don't need to restart.
2209
+ requestStore . renderResumeDataCache = null
2210
+ requestStore . cacheSignal = cacheSignal
2211
+
2212
+ const initialRenderReactController = new AbortController ( )
2213
+
2214
+ const intialRenderDebugChannel =
2215
+ setReactDebugChannel && createDebugChannel ( )
2216
+
2217
+ const initialRscPayload = await getPayload ( )
2218
+ const maybeInitialServerStream = await workUnitAsyncStorage . run (
2176
2219
requestStore ,
2177
- scheduleInSequentialTasks ,
2178
- ( ) => {
2179
- requestStore . prerenderPhase = true
2180
- return ComponentMod . renderToReadableStream (
2181
- RSCPayload ,
2182
- clientReferenceManifest . clientModules ,
2183
- {
2184
- onError : serverComponentsErrorHandler ,
2185
- environmentName : ( ) =>
2186
- requestStore . prerenderPhase === true ? 'Prerender' : 'Server' ,
2187
- filterStackFrame,
2188
- debugChannel : debugChannel ?. serverSide ,
2220
+ ( ) =>
2221
+ pipelineInSequentialTasks (
2222
+ ( ) => {
2223
+ // Static stage
2224
+ requestStore . prerenderPhase = true
2225
+ return ComponentMod . renderToReadableStream (
2226
+ initialRscPayload ,
2227
+ clientReferenceManifest . clientModules ,
2228
+ {
2229
+ onError : serverComponentsErrorHandler ,
2230
+ environmentName,
2231
+ filterStackFrame,
2232
+ debugChannel : intialRenderDebugChannel ?. serverSide ,
2233
+ signal : initialRenderReactController . signal ,
2234
+ }
2235
+ )
2236
+ } ,
2237
+ async ( stream ) => {
2238
+ // Dynamic stage
2239
+ // Note: if we had cache misses, things that would've happened statically otherwise
2240
+ // may be marked as dynamic instead.
2241
+ requestStore . prerenderPhase = false
2242
+
2243
+ // If all cache reads initiated in the static stage have completed,
2244
+ // then all of the necessary caches have to be warm (or there's no caches on the page).
2245
+ // On the other hand, if we still have pending cache reads, then we had a cache miss,
2246
+ // and the static stage didn't render all the content that it normally would have.
2247
+ const hadCacheMiss = cacheSignal . hasPendingReads ( )
2248
+ if ( ! hadCacheMiss ) {
2249
+ // No cache misses. We can use the stream as is.
2250
+ return stream
2251
+ } else {
2252
+ // Cache miss. We'll discard this stream, and render again.
2253
+ return null
2254
+ }
2189
2255
}
2190
2256
)
2191
- } ,
2192
- ( ) => {
2193
- requestStore . prerenderPhase = false
2194
- }
2195
2257
)
2196
2258
2259
+ if ( maybeInitialServerStream !== null ) {
2260
+ // No cache misses. We can use the stream as is.
2261
+
2262
+ // We're using this render, so we should pass its debug channel to the client render.
2263
+ if ( intialRenderDebugChannel ) {
2264
+ setDebugChannelForClientRender ( intialRenderDebugChannel )
2265
+ }
2266
+
2267
+ reactServerResult = new ReactServerResult ( maybeInitialServerStream )
2268
+ } else {
2269
+ // Cache miss. We will use the initial render to fill caches, and discard its result.
2270
+ // Then, we can render again with warm caches.
2271
+
2272
+ // TODO(restart-on-cache-miss):
2273
+ // This might end up waiting for more caches than strictly necessary,
2274
+ // because we can't abort the render yet, and we'll let runtime/dynamic APIs resolve.
2275
+ // Ideally we'd only wait for caches that are needed in the static stage.
2276
+ // This will be optimized in the future by not allowing runtime/dynamic APIs to resolve.
2277
+
2278
+ // During a render, React pings pending tasks using `setImmediate`,
2279
+ // and only waiting for a single `cacheReady` can make us stop filling caches too soon.
2280
+ // To avoid this, we await `cacheReady` repeatedly with an extra delay to let React try render new content
2281
+ // (and potentially discover more caches).
2282
+ await cacheSignal . cacheReadyInRender ( )
2283
+ initialRenderReactController . abort ( )
2284
+
2285
+ //===============================================
2286
+
2287
+ // The initial render acted as a prospective render to warm the caches.
2288
+ // Now, we need to do another render.
2289
+
2290
+ // TODO(restart-on-cache-miss): we should use a separate request store for this instead
2291
+
2292
+ // We've filled the caches, so now we can render as usual.
2293
+ requestStore . prerenderResumeDataCache = null
2294
+ requestStore . renderResumeDataCache = createRenderResumeDataCache (
2295
+ prerenderResumeDataCache
2296
+ )
2297
+ requestStore . cacheSignal = null
2298
+
2299
+ // Reset mutable fields.
2300
+ requestStore . prerenderPhase = undefined
2301
+ requestStore . usedDynamic = undefined
2302
+
2303
+ // The initial render already wrote to its debug channel. We're not using it,
2304
+ // so we need to create a new one.
2305
+ const finalRenderDebugChannel =
2306
+ setReactDebugChannel && createDebugChannel ( )
2307
+ // We know that we won't discard this render, so we can set the debug channel up immediately.
2308
+ if ( finalRenderDebugChannel ) {
2309
+ setDebugChannelForClientRender ( finalRenderDebugChannel )
2310
+ }
2311
+
2312
+ const finalRscPayload = await getPayload ( )
2313
+ const finalServerStream = await workUnitAsyncStorage . run (
2314
+ requestStore ,
2315
+ scheduleInSequentialTasks ,
2316
+ ( ) => {
2317
+ // Static stage
2318
+ requestStore . prerenderPhase = true
2319
+ return ComponentMod . renderToReadableStream (
2320
+ finalRscPayload ,
2321
+ clientReferenceManifest . clientModules ,
2322
+ {
2323
+ onError : serverComponentsErrorHandler ,
2324
+ environmentName,
2325
+ filterStackFrame,
2326
+ debugChannel : finalRenderDebugChannel ?. serverSide ,
2327
+ }
2328
+ )
2329
+ } ,
2330
+ ( ) => {
2331
+ // Dynamic stage
2332
+ requestStore . prerenderPhase = false
2333
+ }
2334
+ )
2335
+
2336
+ reactServerResult = new ReactServerResult ( finalServerStream )
2337
+ }
2338
+
2339
+ // TODO(restart-on-cache-miss):
2340
+ // This can probably be optimized to do less work,
2341
+ // because we've already made sure that we have warm caches.
2197
2342
consoleAsyncStorage . run (
2198
2343
{ dim : true } ,
2199
2344
spawnDynamicValidationInDev ,
@@ -2205,8 +2350,6 @@ async function renderToStream(
2205
2350
requestStore ,
2206
2351
devValidatingFallbackParams
2207
2352
)
2208
-
2209
- reactServerResult = new ReactServerResult ( reactServerStream )
2210
2353
} else {
2211
2354
// This is a dynamic render. We don't do dynamic tracking because we're not prerendering
2212
2355
const RSCPayload = await workUnitAsyncStorage . run (
@@ -2558,12 +2701,21 @@ async function renderToStream(
2558
2701
}
2559
2702
}
2560
2703
2561
- function createDebugChannel ( ) :
2562
- | {
2563
- serverSide : { readable ?: ReadableStream ; writable : WritableStream }
2564
- clientSide : { readable : ReadableStream ; writable ?: WritableStream }
2565
- }
2566
- | undefined {
2704
+ type DebugChannelPair = {
2705
+ serverSide : DebugChannelServer
2706
+ clientSide : DebugChannelClient
2707
+ }
2708
+
2709
+ type DebugChannelServer = {
2710
+ readable ?: ReadableStream < Uint8Array >
2711
+ writable : WritableStream < Uint8Array >
2712
+ }
2713
+ type DebugChannelClient = {
2714
+ readable : ReadableStream < Uint8Array >
2715
+ writable ?: WritableStream < Uint8Array >
2716
+ }
2717
+
2718
+ function createDebugChannel ( ) : DebugChannelPair | undefined {
2567
2719
if ( process . env . NODE_ENV === 'production' ) {
2568
2720
return undefined
2569
2721
}
0 commit comments