@@ -2258,70 +2258,217 @@ async function renderToStream(
2258
2258
// We only have a Prerender environment for projects opted into cacheComponents
2259
2259
experimental . cacheComponents
2260
2260
) {
2261
- // This is a dynamic render. We don't do dynamic tracking because we're not prerendering
2262
- const RSCPayload : InitialRSCPayload & {
2261
+ type RSCPayloadWithValidation = InitialRSCPayload & {
2263
2262
/** Only available during cacheComponents development builds. Used for logging errors. */
2264
2263
_validation ?: Promise < React . ReactNode >
2265
- } = await workUnitAsyncStorage . run (
2266
- requestStore ,
2267
- getRSCPayload ,
2268
- tree ,
2269
- ctx ,
2270
- res . statusCode === 404
2271
- )
2272
- const [ resolveValidation , validationOutlet ] = createValidationOutlet ( )
2273
- RSCPayload . _validation = validationOutlet
2264
+ }
2274
2265
2275
- const debugChannel = setReactDebugChannel && createDebugChannel ( )
2266
+ const getPayload = ( ) : Promise < RSCPayloadWithValidation > =>
2267
+ workUnitAsyncStorage . run (
2268
+ requestStore ,
2269
+ getRSCPayload ,
2270
+ tree ,
2271
+ ctx ,
2272
+ res . statusCode === 404
2273
+ )
2276
2274
2277
- if ( debugChannel ) {
2278
- const [ readableSsr , readableBrowser ] =
2279
- debugChannel . clientSide . readable . tee ( )
2275
+ const environmentName = ( ) =>
2276
+ requestStore . prerenderPhase === true ? 'Prerender' : 'Server'
2280
2277
2281
- reactDebugStream = readableSsr
2278
+ if ( process . env . NEXT_RESTART_ON_CACHE_MISS !== '0' ) {
2279
+ // Try to render the page and see if there's any cache misses.
2280
+ // If there are, wait for caches to finish and restart the render.
2282
2281
2283
- setReactDebugChannel (
2284
- { readable : readableBrowser } ,
2285
- htmlRequestId ,
2286
- requestId
2287
- )
2288
- }
2282
+ const [ resolveValidation , validationOutlet ] = createValidationOutlet ( )
2289
2283
2290
- const reactServerStream = await workUnitAsyncStorage . run (
2291
- requestStore ,
2292
- scheduleInSequentialTasks ,
2293
- ( ) => {
2294
- requestStore . prerenderPhase = true
2295
- return ComponentMod . renderToReadableStream (
2296
- RSCPayload ,
2297
- clientReferenceManifest . clientModules ,
2298
- {
2299
- onError : serverComponentsErrorHandler ,
2300
- environmentName : ( ) =>
2301
- requestStore . prerenderPhase === true ? 'Prerender' : 'Server' ,
2302
- filterStackFrame,
2303
- debugChannel : debugChannel ?. serverSide ,
2284
+ const renderRestartable = async (
2285
+ signal : AbortSignal | undefined ,
2286
+ onPrerenderStageEnd : ( ( ) => void ) | undefined
2287
+ ) => {
2288
+ const rscPayload = await getPayload ( )
2289
+
2290
+ // Placing the validation outlet in the payload is safe
2291
+ // even if we end up discarding this render and restarting,
2292
+ // because it's just an output produced independently.
2293
+ rscPayload . _validation = validationOutlet
2294
+
2295
+ return workUnitAsyncStorage . run (
2296
+ requestStore ,
2297
+ scheduleInSequentialTasks ,
2298
+ ( ) => {
2299
+ // Static stage
2300
+ requestStore . prerenderPhase = true
2301
+ return ComponentMod . renderToReadableStream (
2302
+ rscPayload ,
2303
+ clientReferenceManifest . clientModules ,
2304
+ {
2305
+ onError : serverComponentsErrorHandler ,
2306
+ environmentName,
2307
+ filterStackFrame,
2308
+ // TODO(restart-on-cache-miss): implement `debugChannel`
2309
+ // debugChannel: debugChannel?.serverSide,
2310
+ signal,
2311
+ }
2312
+ )
2313
+ } ,
2314
+ ( ) => {
2315
+ // Dynamic stage
2316
+ requestStore . prerenderPhase = false
2317
+ onPrerenderStageEnd ?.( )
2304
2318
}
2305
2319
)
2306
- } ,
2307
- ( ) => {
2308
- requestStore . prerenderPhase = false
2309
2320
}
2310
- )
2311
2321
2312
- devLogsAsyncStorage . run (
2313
- { dim : true } ,
2314
- spawnDynamicValidationInDev ,
2315
- resolveValidation ,
2316
- tree ,
2317
- ctx ,
2318
- res . statusCode === 404 ,
2319
- clientReferenceManifest ,
2320
- requestStore ,
2321
- devValidatingFallbackParams
2322
- )
2322
+ // This render might end up being used as a prospective render (if there's cache misses),
2323
+ // so we need to set it up for filling caches.
2324
+ const cacheSignal = new CacheSignal ( )
2325
+ const prerenderResumeDataCache = createPrerenderResumeDataCache ( )
2323
2326
2324
- reactServerResult = new ReactServerResult ( reactServerStream )
2327
+ requestStore . prerenderResumeDataCache = prerenderResumeDataCache
2328
+ // `getRenderResumeDataCache` will fall back to using `prerenderResumeDataCache` as `renderResumeDataCache`,
2329
+ // so not having a resume data cache won't break any expectations in case we don't need to restart.
2330
+ requestStore . renderResumeDataCache = null
2331
+ requestStore . cacheSignal = cacheSignal
2332
+
2333
+ const initialRenderReactController = new AbortController ( )
2334
+ const hadCacheMissInStaticStagePromise =
2335
+ createPromiseWithResolvers < boolean > ( )
2336
+
2337
+ console . debug ( `renderToStream (1) :: attempting render` )
2338
+
2339
+ const reactServerStreamPromise = renderRestartable (
2340
+ initialRenderReactController . signal ,
2341
+ ( ) => {
2342
+ console . debug (
2343
+ `renderToStream (1) :: static task finished with ${ cacheSignal [ 'count' ] } caches pending`
2344
+ )
2345
+ // If all cache reads initiated in the static stage have completed,
2346
+ // then either we don't need to fill any caches, or all of them are warm.
2347
+ // On the other hand, if we have pending cache reads, then we had a cache miss.
2348
+ hadCacheMissInStaticStagePromise . resolve (
2349
+ cacheSignal . hasPendingReads ( )
2350
+ )
2351
+ }
2352
+ )
2353
+ reactServerStreamPromise . catch ( ( err ) =>
2354
+ hadCacheMissInStaticStagePromise . reject ( err )
2355
+ )
2356
+
2357
+ const hasCacheMissInStaticStage =
2358
+ await hadCacheMissInStaticStagePromise . promise
2359
+
2360
+ if ( ! hasCacheMissInStaticStage ) {
2361
+ // No cache misses. Use the stream as is.
2362
+ reactServerResult = new ReactServerResult (
2363
+ await reactServerStreamPromise
2364
+ )
2365
+ } else {
2366
+ // Cache miss. We will use the initial render to fill caches, and discard its result.
2367
+ // Then, we can render again with warm caches.
2368
+
2369
+ // TODO(restart-on-cache-miss):
2370
+ // This might end up waiting for more caches than strictly necessary,
2371
+ // because we can't abort the render yet, and we'll let runtime/dynamic APIs resolve.
2372
+ // Ideally we'd only wait for caches that are needed in the static stage.
2373
+ // This will be optimized in the future by not allowing runtime/dynamic APIs to resolve.
2374
+
2375
+ // During a render, React pings pending tasks using `setImmediate`,
2376
+ // and only waiting for a single `cacheReady` can make us stop filling caches too soon.
2377
+ // To avoid this, we await `cacheReady` repeatedly with an extra delay to let React try render new content
2378
+ // (and potentially discover more caches).
2379
+ await cacheSignal . cacheReadyInRender ( )
2380
+ console . debug ( `renderToStream (1) :: cacheReady` )
2381
+ initialRenderReactController . abort ( )
2382
+
2383
+ console . debug (
2384
+ `renderToStream :: restarting render (cache entries: ${ prerenderResumeDataCache . cache . size } )`
2385
+ )
2386
+ // The initial render acted as a prospective render.
2387
+ // Now, we need to clear the state we've set up for it and do a regular render.
2388
+ requestStore . prerenderResumeDataCache = null
2389
+ requestStore . renderResumeDataCache = createRenderResumeDataCache (
2390
+ prerenderResumeDataCache
2391
+ )
2392
+ requestStore . cacheSignal = null
2393
+
2394
+ reactServerResult = new ReactServerResult (
2395
+ await renderRestartable ( undefined , ( ) => {
2396
+ console . debug (
2397
+ `renderToStream (2) :: end of static stage after restart. ${ cacheSignal [ 'count' ] } caches pending`
2398
+ )
2399
+ } )
2400
+ )
2401
+ }
2402
+ // TODO(restart-on-cache-miss):
2403
+ // This can probably be optimized to do less work,
2404
+ // because we've already made sure that we have warm caches.
2405
+ devLogsAsyncStorage . run (
2406
+ { dim : true } ,
2407
+ spawnDynamicValidationInDev ,
2408
+ resolveValidation ,
2409
+ tree ,
2410
+ ctx ,
2411
+ res . statusCode === 404 ,
2412
+ clientReferenceManifest ,
2413
+ requestStore ,
2414
+ devValidatingFallbackParams
2415
+ )
2416
+ } else {
2417
+ const rscPayload = await getPayload ( )
2418
+
2419
+ const [ resolveValidation , validationOutlet ] = createValidationOutlet ( )
2420
+ rscPayload . _validation = validationOutlet
2421
+
2422
+ const debugChannel = setReactDebugChannel && createDebugChannel ( )
2423
+
2424
+ if ( debugChannel ) {
2425
+ const [ readableSsr , readableBrowser ] =
2426
+ debugChannel . clientSide . readable . tee ( )
2427
+
2428
+ reactDebugStream = readableSsr
2429
+
2430
+ setReactDebugChannel (
2431
+ { readable : readableBrowser } ,
2432
+ htmlRequestId ,
2433
+ requestId
2434
+ )
2435
+ }
2436
+
2437
+ const reactServerStream = await workUnitAsyncStorage . run (
2438
+ requestStore ,
2439
+ scheduleInSequentialTasks ,
2440
+ ( ) => {
2441
+ requestStore . prerenderPhase = true
2442
+ return ComponentMod . renderToReadableStream (
2443
+ rscPayload ,
2444
+ clientReferenceManifest . clientModules ,
2445
+ {
2446
+ onError : serverComponentsErrorHandler ,
2447
+ environmentName,
2448
+ filterStackFrame,
2449
+ debugChannel : debugChannel ?. serverSide ,
2450
+ }
2451
+ )
2452
+ } ,
2453
+ ( ) => {
2454
+ requestStore . prerenderPhase = false
2455
+ }
2456
+ )
2457
+
2458
+ devLogsAsyncStorage . run (
2459
+ { dim : true } ,
2460
+ spawnDynamicValidationInDev ,
2461
+ resolveValidation ,
2462
+ tree ,
2463
+ ctx ,
2464
+ res . statusCode === 404 ,
2465
+ clientReferenceManifest ,
2466
+ requestStore ,
2467
+ devValidatingFallbackParams
2468
+ )
2469
+
2470
+ reactServerResult = new ReactServerResult ( reactServerStream )
2471
+ }
2325
2472
} else {
2326
2473
// This is a dynamic render. We don't do dynamic tracking because we're not prerendering
2327
2474
const RSCPayload = await workUnitAsyncStorage . run (
0 commit comments