Warn for non-deterministic "use cache" args during final prerender#92820
Conversation
"use cache" args during final prerender
Tests Passed |
Stats from current PR🔴 1 regression
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles
Server Middleware
Build DetailsBuild Manifests
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📝 Changed Files (16 files)Files with changes:
View diffsapp-page-exp..ntime.dev.jsDiff too large to display app-page-exp..time.prod.jsDiff too large to display app-page-tur..ntime.dev.jsDiff too large to display app-page-tur..time.prod.jsDiff too large to display app-page-tur..ntime.dev.jsDiff too large to display app-page-tur..time.prod.jsDiff too large to display app-page.runtime.dev.jsDiff too large to display app-page.runtime.prod.jsDiff too large to display app-route-ex..ntime.dev.jsDiff too large to display app-route-ex..time.prod.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route-tu..time.prod.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route-tu..time.prod.jsDiff too large to display app-route.runtime.dev.jsDiff too large to display app-route.ru..time.prod.jsDiff too large to display 📎 Tarball URL |
When a `"use cache"` function receives arguments that differ between the cache warming phase and the final prerender, the cache key changes and the Resume Data Cache (RDC) entry from the prospective prerender is missed. This can happen for various reasons, for example when concurrent async operations push results into a shared array in non-deterministic order, and that array is then passed as an argument to a cached function. Without a `cacheSignal` to keep the render alive, the final prerender aborts the cache entry generation, producing an incomplete RSC stream that causes "Connection closed" errors. This change detects that scenario (an RDC miss during the final prerender where `cacheSignal` is `null`) and returns a hanging promise instead of generating a broken cache entry. A warning is logged to help developers identify the non-deterministic arguments. By making the cached function a dynamic hole rather than erroring, the prerender can still complete and produce at least a partial shell if there is a Suspense boundary above. This affects both on-demand prerendering and runtime prefetching.
3fc6183 to
9356114
Compare
9356114 to
c5f13e8
Compare
| break | ||
| // fallthrough | ||
| case 'prerender-runtime': | ||
| if (!cacheSignal) { |
There was a problem hiding this comment.
I wonder if we're at the point of complexity where a separate implementation for the prospective path is easier. nothing to do about it in this PR just leaving as a drive by thought
| at cache (webpack:///<next-src>) | ||
| 1559 | case 'prerender': | ||
| 1560 | case 'prerender-runtime': | ||
| > 1561 | return makeHangingPromise( | ||
| | ^ | ||
| 1562 | workUnitStore.renderSignal, | ||
| 1563 | workStore.route, | ||
| 1564 | 'dynamic "use cache"' |
There was a problem hiding this comment.
Is this really what a user would see? Is this an artifact of testing from within the Next.js repo?
There was a problem hiding this comment.
Webpack has a source mapping bug here. We also had bad source locations like this before in this file. I need to change this assertion the same way I did in #90105 for the previous ones. Will do before merging.
| at Page (app/use-cache-params/[slug]/page.tsx:1:16) | ||
| > 1 | export default async function Page({ | ||
| | ^ | ||
| 2 | params, | ||
| 3 | }: { |
There was a problem hiding this comment.
I think it's because properly returning the hanging promise in the first task of the final prerender now allows React to enhance the owner stack using the hanging promise rejection. Whereas before, we tried to generate a cache entry again due to the RDC cache miss and didn't return the hanging promise in time.
When a
"use cache"function receives arguments that differ between the cache warming phase and the final prerender, the cache key changes and the Resume Data Cache (RDC) entry from the prospective prerender is missed.This can happen for various reasons, for example when concurrent async operations push results into a shared array in non-deterministic order, and that array is then passed as an argument to a cached function.
Without a
cacheSignalto keep the render alive, the final prerender aborts the cache entry generation, producing an incomplete RSC stream that causes "Connection closed" errors.This change detects that scenario (an RDC miss during the final prerender where
cacheSignalisnull) and returns a hanging promise instead of generating a broken cache entry. A warning is logged to help developers identify the non-deterministic arguments. By making the cached function a dynamic hole rather than erroring, the prerender can still complete and produce at least a partial shell if there is a Suspense boundary above. This affects both on-demand prerendering and runtime prefetching.To avoid false positives, cache keys that were intentionally skipped during the prospective prerender (e.g. because the cached function accessed fallback params) are tracked in a
dynamicCacheKeysset on the RDC. During the final prerender, a known dynamic key is returned as a hanging promise early without logging a warning. This also serves as a performance optimization, since it avoids trying to regenerate the entry. This set is intentionally not serialized, as cache misses for dynamic keys should generate fresh entries during the resume at request time.