@@ -2258,70 +2258,217 @@ async function renderToStream(
22582258 // We only have a Prerender environment for projects opted into cacheComponents
22592259 experimental . cacheComponents
22602260 ) {
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 & {
22632262 /** Only available during cacheComponents development builds. Used for logging errors. */
22642263 _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+ }
22742265
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+ )
22762274
2277- if ( debugChannel ) {
2278- const [ readableSsr , readableBrowser ] =
2279- debugChannel . clientSide . readable . tee ( )
2275+ const environmentName = ( ) =>
2276+ requestStore . prerenderPhase === true ? 'Prerender' : 'Server'
22802277
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.
22822281
2283- setReactDebugChannel (
2284- { readable : readableBrowser } ,
2285- htmlRequestId ,
2286- requestId
2287- )
2288- }
2282+ const [ resolveValidation , validationOutlet ] = createValidationOutlet ( )
22892283
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 ?.( )
23042318 }
23052319 )
2306- } ,
2307- ( ) => {
2308- requestStore . prerenderPhase = false
23092320 }
2310- )
23112321
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 ( )
23232326
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+ }
23252472 } else {
23262473 // This is a dynamic render. We don't do dynamic tracking because we're not prerendering
23272474 const RSCPayload = await workUnitAsyncStorage . run (
0 commit comments