@@ -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'
@@ -2122,57 +2126,198 @@ async function renderToStream(
2122
2126
// We only have a Prerender environment for projects opted into cacheComponents
2123
2127
experimental . cacheComponents
2124
2128
) {
2125
- // This is a dynamic render. We don't do dynamic tracking because we're not prerendering
2126
- const RSCPayload : InitialRSCPayload & {
2129
+ type RSCPayloadWithValidation = InitialRSCPayload & {
2127
2130
/** Only available during cacheComponents development builds. Used for logging errors. */
2128
2131
_validation ?: Promise < React . ReactNode >
2129
- } = await workUnitAsyncStorage . run (
2130
- requestStore ,
2131
- getRSCPayload ,
2132
- tree ,
2133
- ctx ,
2134
- res . statusCode === 404
2135
- )
2132
+ }
2133
+
2136
2134
const [ resolveValidation , validationOutlet ] = createValidationOutlet ( )
2137
- RSCPayload . _validation = validationOutlet
2138
2135
2139
- const debugChannel = setReactDebugChannel && createDebugChannel ( )
2136
+ const getPayload = async ( ) : Promise < RSCPayloadWithValidation > => {
2137
+ const payload : RSCPayloadWithValidation =
2138
+ await workUnitAsyncStorage . run (
2139
+ requestStore ,
2140
+ getRSCPayload ,
2141
+ tree ,
2142
+ ctx ,
2143
+ res . statusCode === 404
2144
+ )
2145
+ // Placing the validation outlet in the payload is safe
2146
+ // even if we end up discarding a render and restarting,
2147
+ // because we're not going to wait for the stream to complete,
2148
+ // so leaving the validation unresolved is fine.
2149
+ payload . _validation = validationOutlet
2150
+ return payload
2151
+ }
2140
2152
2141
- if ( debugChannel ) {
2153
+ const setDebugChannelForClientRender = (
2154
+ debugChannel : DebugChannelPair
2155
+ ) => {
2142
2156
const [ readableSsr , readableBrowser ] =
2143
2157
debugChannel . clientSide . readable . tee ( )
2144
2158
2145
2159
reactDebugStream = readableSsr
2146
2160
2147
- setReactDebugChannel (
2161
+ setReactDebugChannel ! (
2148
2162
{ readable : readableBrowser } ,
2149
2163
htmlRequestId ,
2150
2164
requestId
2151
2165
)
2152
2166
}
2153
2167
2154
- const reactServerStream = await workUnitAsyncStorage . run (
2168
+ const environmentName = ( ) =>
2169
+ requestStore . prerenderPhase === true ? 'Prerender' : 'Server'
2170
+
2171
+ // Try to render the page and see if there's any cache misses.
2172
+ // If there are, wait for caches to finish and restart the render.
2173
+
2174
+ // This render might end up being used as a prospective render (if there's cache misses),
2175
+ // so we need to set it up for filling caches.
2176
+ const cacheSignal = new CacheSignal ( )
2177
+
2178
+ // If we encounter async modules that delay rendering, we'll also need to restart.
2179
+ // TODO(restart-on-cache-miss): technically, we only need to wait for pending *server* modules here,
2180
+ // but `trackPendingModules` doesn't distinguish between client and server.
2181
+ trackPendingModulesInRender ( cacheSignal )
2182
+
2183
+ const prerenderResumeDataCache = createPrerenderResumeDataCache ( )
2184
+
2185
+ requestStore . prerenderResumeDataCache = prerenderResumeDataCache
2186
+ // `getRenderResumeDataCache` will fall back to using `prerenderResumeDataCache` as `renderResumeDataCache`,
2187
+ // so not having a resume data cache won't break any expectations in case we don't need to restart.
2188
+ requestStore . renderResumeDataCache = null
2189
+ requestStore . cacheSignal = cacheSignal
2190
+
2191
+ const initialRenderReactController = new AbortController ( )
2192
+
2193
+ const intialRenderDebugChannel =
2194
+ setReactDebugChannel && createDebugChannel ( )
2195
+
2196
+ const initialRscPayload = await getPayload ( )
2197
+ const maybeInitialServerStream = await workUnitAsyncStorage . run (
2155
2198
requestStore ,
2156
- scheduleInSequentialTasks ,
2157
- ( ) => {
2158
- requestStore . prerenderPhase = true
2159
- return ComponentMod . renderToReadableStream (
2160
- RSCPayload ,
2161
- clientReferenceManifest . clientModules ,
2162
- {
2163
- onError : serverComponentsErrorHandler ,
2164
- environmentName : ( ) =>
2165
- requestStore . prerenderPhase === true ? 'Prerender' : 'Server' ,
2166
- filterStackFrame,
2167
- debugChannel : debugChannel ?. serverSide ,
2199
+ ( ) =>
2200
+ pipelineInSequentialTasks (
2201
+ ( ) => {
2202
+ // Static stage
2203
+ requestStore . prerenderPhase = true
2204
+ return ComponentMod . renderToReadableStream (
2205
+ initialRscPayload ,
2206
+ clientReferenceManifest . clientModules ,
2207
+ {
2208
+ onError : serverComponentsErrorHandler ,
2209
+ environmentName,
2210
+ filterStackFrame,
2211
+ debugChannel : intialRenderDebugChannel ?. serverSide ,
2212
+ signal : initialRenderReactController . signal ,
2213
+ }
2214
+ )
2215
+ } ,
2216
+ async ( stream ) => {
2217
+ // Dynamic stage
2218
+ // Note: if we had cache misses, things that would've happened statically otherwise
2219
+ // may be marked as dynamic instead.
2220
+ requestStore . prerenderPhase = false
2221
+
2222
+ // If all cache reads initiated in the static stage have completed,
2223
+ // then all of the necessary caches have to be warm (or there's no caches on the page).
2224
+ // On the other hand, if we still have pending cache reads, then we had a cache miss,
2225
+ // and the static stage didn't render all the content that it normally would have.
2226
+ const hadCacheMiss = cacheSignal . hasPendingReads ( )
2227
+ if ( ! hadCacheMiss ) {
2228
+ // No cache misses. We can use the stream as is.
2229
+ return stream
2230
+ } else {
2231
+ // Cache miss. We'll discard this stream, and render again.
2232
+ return null
2233
+ }
2168
2234
}
2169
2235
)
2170
- } ,
2171
- ( ) => {
2172
- requestStore . prerenderPhase = false
2173
- }
2174
2236
)
2175
2237
2238
+ if ( maybeInitialServerStream !== null ) {
2239
+ // No cache misses. We can use the stream as is.
2240
+
2241
+ // We're using this render, so we should pass its debug channel to the client render.
2242
+ if ( intialRenderDebugChannel ) {
2243
+ setDebugChannelForClientRender ( intialRenderDebugChannel )
2244
+ }
2245
+
2246
+ reactServerResult = new ReactServerResult ( maybeInitialServerStream )
2247
+ } else {
2248
+ // Cache miss. We will use the initial render to fill caches, and discard its result.
2249
+ // Then, we can render again with warm caches.
2250
+
2251
+ // TODO(restart-on-cache-miss):
2252
+ // This might end up waiting for more caches than strictly necessary,
2253
+ // because we can't abort the render yet, and we'll let runtime/dynamic APIs resolve.
2254
+ // Ideally we'd only wait for caches that are needed in the static stage.
2255
+ // This will be optimized in the future by not allowing runtime/dynamic APIs to resolve.
2256
+
2257
+ // During a render, React pings pending tasks using `setImmediate`,
2258
+ // and only waiting for a single `cacheReady` can make us stop filling caches too soon.
2259
+ // To avoid this, we await `cacheReady` repeatedly with an extra delay to let React try render new content
2260
+ // (and potentially discover more caches).
2261
+ await cacheSignal . cacheReadyInRender ( )
2262
+ initialRenderReactController . abort ( )
2263
+
2264
+ //===============================================
2265
+
2266
+ // The initial render acted as a prospective render to warm the caches.
2267
+ // Now, we need to do another render.
2268
+
2269
+ // TODO(restart-on-cache-miss): we should use a separate request store for this instead
2270
+
2271
+ // We've filled the caches, so now we can render as usual.
2272
+ requestStore . prerenderResumeDataCache = null
2273
+ requestStore . renderResumeDataCache = createRenderResumeDataCache (
2274
+ prerenderResumeDataCache
2275
+ )
2276
+ requestStore . cacheSignal = null
2277
+
2278
+ // Reset mutable fields.
2279
+ requestStore . prerenderPhase = undefined
2280
+ requestStore . usedDynamic = undefined
2281
+
2282
+ // The initial render already wrote to its debug channel. We're not using it,
2283
+ // so we need to create a new one.
2284
+ const finalRenderDebugChannel =
2285
+ setReactDebugChannel && createDebugChannel ( )
2286
+ // We know that we won't discard this render, so we can set the debug channel up immediately.
2287
+ if ( finalRenderDebugChannel ) {
2288
+ setDebugChannelForClientRender ( finalRenderDebugChannel )
2289
+ }
2290
+
2291
+ const finalRscPayload = await getPayload ( )
2292
+ const finalServerStream = await workUnitAsyncStorage . run (
2293
+ requestStore ,
2294
+ scheduleInSequentialTasks ,
2295
+ ( ) => {
2296
+ // Static stage
2297
+ requestStore . prerenderPhase = true
2298
+ return ComponentMod . renderToReadableStream (
2299
+ finalRscPayload ,
2300
+ clientReferenceManifest . clientModules ,
2301
+ {
2302
+ onError : serverComponentsErrorHandler ,
2303
+ environmentName,
2304
+ filterStackFrame,
2305
+ debugChannel : finalRenderDebugChannel ?. serverSide ,
2306
+ }
2307
+ )
2308
+ } ,
2309
+ ( ) => {
2310
+ // Dynamic stage
2311
+ requestStore . prerenderPhase = false
2312
+ }
2313
+ )
2314
+
2315
+ reactServerResult = new ReactServerResult ( finalServerStream )
2316
+ }
2317
+
2318
+ // TODO(restart-on-cache-miss):
2319
+ // This can probably be optimized to do less work,
2320
+ // because we've already made sure that we have warm caches.
2176
2321
devLogsAsyncStorage . run (
2177
2322
{ dim : true } ,
2178
2323
spawnDynamicValidationInDev ,
@@ -2184,8 +2329,6 @@ async function renderToStream(
2184
2329
requestStore ,
2185
2330
devValidatingFallbackParams
2186
2331
)
2187
-
2188
- reactServerResult = new ReactServerResult ( reactServerStream )
2189
2332
} else {
2190
2333
// This is a dynamic render. We don't do dynamic tracking because we're not prerendering
2191
2334
const RSCPayload = await workUnitAsyncStorage . run (
@@ -2539,12 +2682,21 @@ async function renderToStream(
2539
2682
}
2540
2683
}
2541
2684
2542
- function createDebugChannel ( ) :
2543
- | {
2544
- serverSide : { readable ?: ReadableStream ; writable : WritableStream }
2545
- clientSide : { readable : ReadableStream ; writable ?: WritableStream }
2546
- }
2547
- | undefined {
2685
+ type DebugChannelPair = {
2686
+ serverSide : DebugChannelServer
2687
+ clientSide : DebugChannelClient
2688
+ }
2689
+
2690
+ type DebugChannelServer = {
2691
+ readable ?: ReadableStream < Uint8Array >
2692
+ writable : WritableStream < Uint8Array >
2693
+ }
2694
+ type DebugChannelClient = {
2695
+ readable : ReadableStream < Uint8Array >
2696
+ writable ?: WritableStream < Uint8Array >
2697
+ }
2698
+
2699
+ function createDebugChannel ( ) : DebugChannelPair | undefined {
2548
2700
if ( process . env . NODE_ENV === 'production' ) {
2549
2701
return undefined
2550
2702
}
0 commit comments