Support accessing root params in "use cache" functions#91191
Support accessing root params in "use cache" functions#91191unstubbable merged 3 commits intocanaryfrom
"use cache" functions#91191Conversation
Tests Passed |
4a1b101 to
b93df52
Compare
900705d to
3b3d69a
Compare
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.jsfailed to diffapp-page-exp..time.prod.jsDiff too large to display app-page-tur..ntime.dev.jsfailed to diffapp-page-tur..time.prod.jsfailed to diffapp-page-tur..ntime.dev.jsfailed to diffapp-page-tur..time.prod.jsDiff too large to display app-page.runtime.dev.jsfailed to diffapp-page.runtime.prod.jsDiff too large to display app-route-ex..ntime.dev.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route.runtime.dev.jsDiff too large to display pages-api-tu..ntime.dev.jsDiff too large to display pages-api.runtime.dev.jsDiff too large to display pages-turbo...ntime.dev.jsDiff too large to display pages.runtime.dev.jsDiff too large to display 📎 Tarball URL |
e993281 to
4476ebe
Compare
0e62923 to
88960d2
Compare
3b3d69a to
0a35f46
Compare
88960d2 to
cd95606
Compare
0a35f46 to
5bc448f
Compare
cd95606 to
a0d54b5
Compare
5bc448f to
f36358a
Compare
f459f4e to
af0bc89
Compare
f36358a to
381a437
Compare
| // The RDC is per-page and root params are fixed within a page, so we always | ||
| // use the coarse key (without root param suffix). Unlike the cache handler, | ||
| // the RDC doesn't need root-param-specific keys for isolation. | ||
| prerenderResumeDataCache.cache.set(serializedCacheKey, rdcResult) |
There was a problem hiding this comment.
i guess this is fine, just feels like something that might bite us later...
There was a problem hiding this comment.
Why? It's the same premise that allows us to put 'use cache: private' into the RDC without including the read cookies in the cache key, isn't it?
There was a problem hiding this comment.
the 'use cache: private' entries aren't persisted anywhere, so it's less observable.
this here will be persisted along the prerender. i think previously the keys in the serialized RDC would always be equivalent to the keys the cache handler has, but now that'll change.
so If we did something like, idk, seeding the in-memory cache using entries from RDC on first request, that could become a problem, because we might accidentally seed the coarse entry (which should contain a redirect) with data for a fine entry UNLESS you remember this special case
(that specific use-case likely wouldn't make sense, bc we can just read from RDC instead, but i can imagine this shape of problem coming up in the future in some way)
i don't think this is a blocker, it'll work fine, but i guess i'm just wondering if we need to introduce this fork in behavior?
There was a problem hiding this comment.
It avoids having to add redirect entries to the RDC as well, which avoids runtime overhead and keeps the ISR payload size smaller.
| // read no root params, a previous invocation may have — and we need to | ||
| // match the lookup key from that union. |
There was a problem hiding this comment.
and we need to match the lookup key from that union.
this thing about matching the lookup key is a bit confusing to me, because if the current invocation read more root params than a past one, we also wouldn't match the old key, so what happens then?
There was a problem hiding this comment.
Can you rephrase that question, maybe spelling out a concrete scenario that you have in mind?
There was a problem hiding this comment.
The comment talks about a scenario where past invocations read more params than the current invocation. It doesn't say what should happen if it's the opposite, and the current invocation read more params than past ones. It seems like in both cases we'd have a potential mismatch that needs handling, so why does this place only call out the former, but not the latter?
There was a problem hiding this comment.
I thought using readRootParamNames (the case where we might have read more than previously) was kinda obvious, so I only called out the less obvious case where we also need to include the previously read root params.
381a437 to
9bec0cc
Compare
af0bc89 to
f50f505
Compare
8a7b9c8 to
daa3b2b
Compare
Root params (e.g. `import { lang } from 'next/root-params'`) can now be
read inside `"use cache"` functions. The read root param values are
automatically included in the cache key so that different root param
combinations produce separate cache entries.
Since which root params a cache function reads is only known after
execution, the cache key is reconciled post-generation. When root params
are read, a two-key scheme is used: the full entry is stored under a
specific key (coarse key + root param suffix), and a lightweight
redirect entry is stored under the coarse key. The redirect entry's tags
encode the root param names using the pattern `_N_RP_<rootParamName>`
(e.g. `_N_RP_lang`), following the convention of existing internal tags
like `_N_T_<pathname>` (e.g. `_N_T_/dashboard`) for implicit route tags.
This allows a cold server to resolve the specific key on the first
request after restart. An in-memory map (`knownRootParamsByFunctionId`)
provides a fast path for subsequent invocations. When no root params are
read, the full entry is stored directly under the coarse key with no
redirect involved.
The in-memory map grows monotonically — if a function conditionally
reads different root params across invocations, the set accumulates all
observed param names. The redirect entry's tags are built from this
combined set, ensuring cold servers always resolve the most complete
specific key.
The two-key scheme only applies to the cache handler. The Resume Data
Cache (RDC) always uses the coarse key because each page gets its own
isolated RDC instance, so root params are fixed within a single RDC and
no disambiguation is needed. When an RDC entry is found during resume,
it seeds `knownRootParamsByFunctionId` so that subsequent cache handler
lookups can use the specific key directly.
Reading root params inside `unstable_cache` still throws. Reading root
params inside `"use cache"` nested within `unstable_cache` throws with a
specific error message explaining the limitation.
Alternatives considered: extending the `CacheEntry` interface (would be
a breaking change for custom cache handlers), encoding root param
metadata in the stream via a wrapper object, or sentinel byte, or
Flightception (runtime overhead of stream manipulation on every cache
read), and deferring `cacheHandler.set` until after generation (breaks
the cache handler's pending-set deduplication for concurrent requests).
daa3b2b to
6282a5a
Compare

Root params (e.g.
import { lang } from 'next/root-params') can now be read inside"use cache"functions. The read root param values are automatically included in the cache key so that different root param combinations produce separate cache entries.Since which root params a cache function reads is only known after execution, the cache key is reconciled post-generation. When root params are read, a two-key scheme is used: the full entry is stored under a specific key (coarse key + root param suffix), and a lightweight redirect entry is stored under the coarse key. The redirect entry's tags encode the root param names using the pattern
_N_RP_<rootParamName>(e.g._N_RP_lang), following the convention of existing internal tags like_N_T_<pathname>(e.g._N_T_/dashboard) for implicit route tags. This allows a cold server to resolve the specific key on the first request after restart. An in-memory map (knownRootParamsByFunctionId) provides a fast path for subsequent invocations. When no root params are read, the full entry is stored directly under the coarse key with no redirect involved.The in-memory map grows monotonically — if a function conditionally reads different root params across invocations, the set accumulates all observed param names. The redirect entry's tags are built from this combined set, ensuring cold servers always resolve the most complete specific key.
The two-key scheme only applies to the cache handler. The Resume Data Cache (RDC) always uses the coarse key because each page gets its own isolated RDC instance, so root params are fixed within a single RDC and no disambiguation is needed. When an RDC entry is found during resume, it seeds
knownRootParamsByFunctionIdso that subsequent cache handler lookups can use the specific key directly.Reading root params inside
unstable_cachestill throws. Reading root params inside"use cache"nested withinunstable_cachethrows with a specific error message explaining the limitation.Alternatives considered: extending the
CacheEntryinterface (would be a breaking change for custom cache handlers), encoding root param metadata in the stream via a wrapper object, or sentinel byte, or Flightception (runtime overhead of stream manipulation on every cache read), and deferringcacheHandler.setuntil after generation (breaks the cache handler's pending-set deduplication for concurrent requests).