Skip to content

Commit 32ee993

Browse files
committed
wip
1 parent 2641bae commit 32ee993

File tree

5 files changed

+238
-57
lines changed

5 files changed

+238
-57
lines changed

packages/next/src/build/templates/app-page.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,8 @@ export async function handler(
409409
if (
410410
nextConfig.experimental.cacheComponents &&
411411
!isPrefetchRSCRequest &&
412-
!context.renderOpts.isPossibleServerAction
412+
!context.renderOpts.isPossibleServerAction &&
413+
process.env.NEXT_RESTART_ON_CACHE_MISS === '0'
413414
) {
414415
const warmup = await routeModule.warmup(nextReq, nextRes, context)
415416

packages/next/src/server/app-render/app-render.tsx

Lines changed: 199 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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(

packages/next/src/server/app-render/cache-signal.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* and should only be used in codepaths gated with this feature.
66
*/
77

8+
import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler'
89
import { InvariantError } from '../../shared/lib/invariant-error'
910

1011
export class CacheSignal {
@@ -79,6 +80,23 @@ export class CacheSignal {
7980
})
8081
}
8182

83+
/**
84+
* Like `cacheReady`, but for use when rendering (not prerendering).
85+
* React schedules work differently between these two, which affects the timing
86+
* of waiting for all caches to be discovered.
87+
**/
88+
async cacheReadyInRender() {
89+
// During a render, React pings pending tasks (that are waiting for something async to resolve) using `setImmediate`.
90+
// This is unlike a prerender, where they are pinged in a microtask.
91+
// This means that, if we're warming caches via a render (not a prerender),
92+
// we need to give React more time to continue rendering after a cache has resolved
93+
// in order to make sure we've discovered all the caches needed for the current render.
94+
do {
95+
await this.cacheReady()
96+
await waitAtLeastOneReactRenderTask()
97+
} while (this.count > 0)
98+
}
99+
82100
beginRead() {
83101
this.count++
84102

packages/next/src/server/app-render/work-unit-async-storage.external.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export interface RequestStore extends CommonWorkUnitStore {
6969
usedDynamic?: boolean
7070
prerenderPhase?: boolean
7171
devFallbackParams?: OpaqueFallbackRouteParams | null
72+
cacheSignal?: CacheSignal | null
73+
prerenderResumeDataCache?: PrerenderResumeDataCache | null
7274
}
7375

7476
/**
@@ -351,8 +353,14 @@ export function getPrerenderResumeDataCache(
351353
// TODO eliminate fetch caching in client scope and stop exposing this data
352354
// cache during SSR.
353355
return workUnitStore.prerenderResumeDataCache
356+
case 'request': {
357+
// In dev, we might fill caches even during a dynamic request.
358+
if (workUnitStore.prerenderResumeDataCache) {
359+
return workUnitStore.prerenderResumeDataCache
360+
}
361+
// fallthrough
362+
}
354363
case 'prerender-legacy':
355-
case 'request':
356364
case 'cache':
357365
case 'private-cache':
358366
case 'unstable-cache':
@@ -367,7 +375,6 @@ export function getRenderResumeDataCache(
367375
): RenderResumeDataCache | null {
368376
switch (workUnitStore.type) {
369377
case 'request':
370-
return workUnitStore.renderResumeDataCache
371378
case 'prerender':
372379
case 'prerender-runtime':
373380
case 'prerender-client':
@@ -380,7 +387,7 @@ export function getRenderResumeDataCache(
380387
case 'prerender-ppr':
381388
// Otherwise we return the mutable resume data cache here as an immutable
382389
// version of the cache as it can also be used for reading.
383-
return workUnitStore.prerenderResumeDataCache
390+
return workUnitStore.prerenderResumeDataCache ?? null
384391
case 'cache':
385392
case 'private-cache':
386393
case 'unstable-cache':
@@ -503,9 +510,15 @@ export function getCacheSignal(
503510
case 'prerender-client':
504511
case 'prerender-runtime':
505512
return workUnitStore.cacheSignal
513+
case 'request': {
514+
// In dev, we might fill caches even during a dynamic request.
515+
if (workUnitStore.cacheSignal) {
516+
return workUnitStore.cacheSignal
517+
}
518+
// fallthrough
519+
}
506520
case 'prerender-ppr':
507521
case 'prerender-legacy':
508-
case 'request':
509522
case 'cache':
510523
case 'private-cache':
511524
case 'unstable-cache':

packages/next/src/server/use-cache/use-cache-wrapper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ async function collectResult(
466466
// then it shouldn't have any effects on the prerender. We'll decide
467467
// whether or not this cache should have its life & tags propagated when
468468
// we read the entry in the final prerender from the resume data cache.
469+
470+
// TODO(restart-on-cache-miss): might need to do the same here?
469471
break
470472
}
471473
case 'request':

0 commit comments

Comments
 (0)