next.js
1ff6a803 - Reuse completed cache entries for the rest of a request (#96727)

Commit
4 days ago
Reuse completed cache entries for the rest of a request (#96727) Calling the same `'use cache: private'` function twice in one request executed its body twice in production. Preloading at the top of a segment and reading the same function again lower down for composability, which is the shape that motivates a preload in the first place, therefore did the work twice instead of once. The intra-request dedupe map dropped an entry as soon as its fill completed, so it only ever covered concurrent invocations. A later invocation fell through to the cache handler, and the `React.cache` memo wrapping every cache function missed whenever the arguments were not reference-equal. Public caches got a handler hit out of that, but private caches have no handler in production and their entries are excluded from the immutable Resume Data Cache of a dynamic request, so nothing had stored the entry. Completed invocations now move into `completedCacheInvocations` on the work store instead of being dropped, and a later invocation joins that entry. The pending map keeps its previous semantics untouched, because a concurrent joiner shares a fill that is genuinely in flight and must not re-run the discard checks against it, whereas a completed entry is a stored value and is only reused when the caller has not asked to bypass caches and nothing has invalidated it since. Private caches still get no cache handler, so the map is what backs them, and it lives on the work store and so cannot carry request-derived data beyond the request that produced it. Retention is limited to the kinds where it saves real work: private caches, and kinds whose handler came from the platform or from `cacheHandlers` config, where a read can be a network round trip. `isBuiltInCacheHandler` answers that by tracking the handler instances `initializeCacheHandlers` constructs itself. Recording instances rather than kinds is what keeps aliasing right, since a self-hosted `remote` resolves to the very same in-memory handler as `default` and retaining its entries would duplicate that cache for nothing, while a platform-supplied `remote` does not. The `use-cache-custom-handler` suite covers the mechanism rather than the observable value, counting `::get` calls to assert that two sequential reads reach the handler once. Value equality alone cannot distinguish a retained entry from a handler hit, which is also why the new `dedup-sequential` fixture passes without this change and serves only as a regression pin next to `private-dedup-sequential`.
Author
Parents
Loading