Skip to content

Commit f06d951

Browse files
lubieowocegnoff
andauthored
[Cache Components] Dev - restart render on cache miss (#84088)
This PR replaces the previous approach to the dev-time cache warmup. On full-page requests, we 1. We attempt an initial RSC render. It uses a RequestStore, but includes a `cacheSignal` and a `prerenderResumeDataCache` to be filled 1. if there's no cache misses, we use the RSC render as is, and move onto SSR 2. If there's any cache misses in the static stage (i.e. during the first timeout), we treat the inital render as a prospective render, use it only for filling caches, and discard the result 3. Once caches are filled, we render RSC again (using a fresh RequestStore, with a filled `renderResumeDataCache`), and use this second stream for SSR instead. With this strategy, we minimize the amount of work we need to do for cache warming -- once caches for a page are filled, we can render it in one go, with no separate cache-filling render necessary. --- A lot of the effort here goes into trying to reflect the behavior of a static prerender into what we do during a dynamic render -- if something would be a dynamic hole (hanging promise) in a prerender, we shouldn't resolve it microtaskily (in the static stage). Instead, we have to delay it into a future timeout (the dynamic stage). In this PR, we're still using `makeDevtoolsIOAwarePromise` for this (i.e. just `new Promise((resolve) => setTimeout(resolve))`), though this will change to a more precise mechanism in #84644. The timing of when promises resolve is currently tested in `cache-components.dev-warmup.test.ts`, where we check the environment labels on the server logs replayed in the browser, and use that to verify which "phase" (Static/Dynamic) a given API resolves in. This will be the foundation for prefetch validation, where we'll need to snapshot what was rendered in each stage (Static/Runtime/Dynamic) and use that to validate whether a prefetch would result in an instant navigation. Note that there's currently a bug involving `params` and `searchParams` -- they can currently incorrectly resolve in the static phase (because those promises are created before we start the actual render). We're not (yet) relying on the timing of these promises for anything critical, so it's fine to leave it for now. This bug will be addressed in #84644, where I introduce a more precise mechanism for controlling the timing of promise resolution, which also lets us separate "runtime" APIs like `cookies` into a separate phase. --- I've also left the current `spawnDynamicValidationInDev` codepath as is, so after the we're done with all the render restarting, we'll still kick off a validation prerender. This will also change in the future (and could be optimized -- we've already ensured that all the caches are filled, so we could e.g. skip the prospective render there) but I'm trying not to do everything at once. --------- Co-authored-by: Josh Story <[email protected]>
1 parent ef16156 commit f06d951

File tree

31 files changed

+1179
-414
lines changed

31 files changed

+1179
-414
lines changed

packages/next/errors.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,5 +872,7 @@
872872
"871": "Image with src \"%s\" is using a query string which is not configured in images.localPatterns.\\nRead more: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns",
873873
"872": "updateTag can only be called from within a Server Action. To invalidate cache tags in Route Handlers or other contexts, use revalidateTag instead. See more info here: https://nextjs.org/docs/app/api-reference/functions/updateTag",
874874
"873": "Invalid profile provided \"%s\" must be configured under cacheLife in next.config or be \"max\"",
875-
"874": "Expected not to install Node.js global behaviors in the edge runtime."
875+
"874": "Expected not to install Node.js global behaviors in the edge runtime.",
876+
"875": "`pipelineInSequentialTasks` should not be called in edge runtime.",
877+
"876": "dynamicInDevStagedRendering should only be used in development mode and when Cache Components is enabled."
876878
}

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

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -402,26 +402,6 @@ export async function handler(
402402
const nextReq = new NodeNextRequest(req)
403403
const nextRes = new NodeNextResponse(res)
404404

405-
// TODO: adapt for putting the RDC inside the postponed data
406-
// If we're in dev, and this isn't a prefetch or a server action,
407-
// we should seed the resume data cache.
408-
if (process.env.NODE_ENV === 'development') {
409-
if (
410-
nextConfig.experimental.cacheComponents &&
411-
!isPrefetchRSCRequest &&
412-
!context.renderOpts.isPossibleServerAction
413-
) {
414-
const warmup = await routeModule.warmup(nextReq, nextRes, context)
415-
416-
// If the warmup is successful, we should use the resume data
417-
// cache from the warmup.
418-
if (warmup.metadata.renderResumeDataCache) {
419-
context.renderOpts.renderResumeDataCache =
420-
warmup.metadata.renderResumeDataCache
421-
}
422-
}
423-
}
424-
425405
return routeModule.render(nextReq, nextRes, context).finally(() => {
426406
if (!span) return
427407

packages/next/src/export/routes/app-page.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ export async function exportAppPage(
8484
fallbackRouteParams,
8585
renderOpts,
8686
undefined,
87-
false,
8887
sharedContext
8988
)
9089

packages/next/src/server/app-render/app-render-render-utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,40 @@ export function scheduleInSequentialTasks<R>(
2929
})
3030
}
3131
}
32+
33+
/**
34+
* This is a utility function to make scheduling sequential tasks that run back to back easier.
35+
* We schedule on the same queue (setTimeout) at the same time to ensure no other events can sneak in between.
36+
* The function that runs in the second task gets access to the first tasks's result.
37+
*/
38+
export function pipelineInSequentialTasks<A, B>(
39+
render: () => A,
40+
followup: (a: A) => B | Promise<B>
41+
): Promise<B> {
42+
if (process.env.NEXT_RUNTIME === 'edge') {
43+
throw new InvariantError(
44+
'`pipelineInSequentialTasks` should not be called in edge runtime.'
45+
)
46+
} else {
47+
return new Promise((resolve, reject) => {
48+
let renderResult: A | undefined = undefined
49+
setTimeout(() => {
50+
try {
51+
renderResult = render()
52+
} catch (err) {
53+
clearTimeout(followupId)
54+
reject(err)
55+
}
56+
}, 0)
57+
const followupId = setTimeout(() => {
58+
// if `render` threw, then the `followup` timeout would've been cleared,
59+
// so if we got here, we're guaranteed to have a `renderResult`.
60+
try {
61+
resolve(followup(renderResult!))
62+
} catch (err) {
63+
reject(err)
64+
}
65+
}, 0)
66+
})
67+
}
68+
}

0 commit comments

Comments
 (0)