Hendrik Liebau
mail@hendrik-liebau.de
90d · built 2026-05-28
90-day totals
- Commits
- 71
- Grow
- 6.7
- Maintenance
- 7.2
- Fixes
- 4.8
- Total ETV
- 18.6
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 63 %
- By Growth share
- Top 80 %
30-day trajectory
Last 30 days vs. the 30 days before. Up arrows on Growth and ETV mean improvement; up arrow on Fixes share means more time on fixes (worse).
Daily performance
Daily ETV, stacked by Growth, Maintenance and Fixes.
Work-mix over time
Share of Growth / Maintenance / Fixes over a rolling 7-day window. Reads as 'where is effort flowing right now'.
Bug flow over time
Monthly bug flow attributed to this developer. The left bar (red) is bug impact this dev authored that was addressed in the given month — combining bugs others fixed for them and bugs they fixed themselves. The right bar is fixes they personally shipped that month, split between self-fixes (overlap with the red bar) and fixes done for someone else. X-axis is fix-time, not introduction-time — the Navigara API attributes bugs backward to the author at the moment the fix lands.
- Self-fix share
- 18%
- Bugs you introduced
- 6.8
- Bugs you fixed
- 17.6
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
Most impactful commits
Top 20 by ETV in the 90-day window.
- 1.5ETVRevert legacy PPR removal (#90948) This caused regressions. Apparently it was not a clean removal.github.com-vercel-next.js · c2bba0ea · 2026-03-05
- 1.4ETVAdd Flight SSR benchmark fixture (#36180) This PR adds a benchmark fixture for measuring the performance overhead of the React Server Components (RSC) Flight rendering compared to plain Fizz server-side rendering. ### Motivation Performance discussions around RSC (e.g. #36143, #35125) have highlighted the need for reproducible benchmarks that accurately measure the cost that Flight adds on top of Fizz. This fixture provides multiple benchmark modes that can be used to track performance improvements across commits, compare Node vs Edge (web streams) overhead, and identify bottlenecks in Flight serialization and deserialization. ### What it measures The benchmark renders a dashboard app with ~25 components (16 client components), 200 product rows with nested data (~325KB Flight payload), and ~250 Suspense boundaries in the async variant. It compares 8 render variants: Fizz-only and Flight+Fizz, across Node and Edge stream APIs, with both synchronous and asynchronous apps. ### Benchmark modes - **`yarn bench`** runs a sequential in-process benchmark with realistic Flight script injection (tee + `TransformStream`/`Transform` buffered injection), matching what real frameworks do when inlining the RSC payload into the HTML response for hydration. - **`yarn bench:bare`** runs the same benchmark without script injection, isolating the React-internal rendering cost. This is best for tracking changes to Flight serialization or Fizz rendering. - **`yarn bench:server`** starts an HTTP server and uses `autocannon` to measure real req/s at `c=1` and `c=10`. The `c=1` results provide a clean signal for tracking React-internal changes, while `c=10` reflects throughput under concurrent load. - **`yarn bench:concurrent`** runs an in-process concurrent benchmark with 50 in-flight renders via `Promise.all`, measuring throughput without HTTP overhead. - **`yarn bench:profile`** collects CPU profiles via the V8 inspector and reports the top functions by self-time along with GC pause data. - **`yarn start`** starts the HTTP server for manual browser testing. Appending `.rsc` to any Flight URL serves the raw Flight payload. ### Key findings during development On Node 22, the Flight+Fizz overhead compared to Fizz-only rendering is roughly: - **Without script injection** (`bench:bare`): ~2.2x for sync, ~1.3x for async - **With script injection** (`bench:server`, c=1): ~2.9x for sync, ~1.8x for async - **Edge vs Node** adds another ~30% for sync and ~10% for async, driven by the stream plumbing for script injection (tee + `TransformStream` buffering) The async variant better represents real-world applications where server components fetch data asynchronously. Its lower overhead reflects the fact that Flight serialization and Fizz rendering can overlap with I/O wait times, making the added Flight cost a smaller fraction of total request time. The benchmark also revealed that the Edge vs Node gap is negligible for Fizz-only rendering (~1-2%) but grows to ~15% for Flight+Fizz sync even without script injection. With script injection (tee + `TransformStream` buffering), the gap roughly doubles to ~30% for sync. The async variants show smaller gaps (~5% without, ~10% with injection).github.com-facebook-react · 1b45e243 · 2026-04-02
- 1.1ETVDetect `'use cache'` module-scope deadlocks early in dev (#93500) When a `'use cache'` fill stalls in dev today, the user has to wait the full `useCacheTimeout` — 54 seconds by default — before any error surfaces, and the resulting `UseCacheTimeoutError` is too generic to point at the cause. A common cause is module-scoped state that ends up joining a promise from the outer render scope — for example a top-level `Map<string, Promise>` used to dedupe fetches, where the cache body and the outer scope both await the same promise. That promise hangs because Next.js intentionally converts uncached fetches into hanging promises during prerendering (and in dev while filling caches in the static or runtime stage), so the cache function ends up waiting forever on an outer-scope fetch that will never resolve. Most users reload long before the timeout appears, so they never see any signal that something specific is wrong. This change adds a dev-only probe that surfaces this class of deadlock earlier. Once a cache fill has been idle for ten seconds, the dev server re-runs the same cache function in a worker thread with a fresh module scope. If it completes there, the hang in the main process is attributable to outer-scope state, and we abort the fill with `UseCacheDeadlockError` whose message points the user at the dedupe pattern and how to fix it. If the probe also hangs or fails for any other reason — decode failure, missing module, the body throwing — we fall back to the regular cache-fill timeout, so the probe is a positive signal only and never false-positives a deadlock. The probe is gated on `__NEXT_DEV_SERVER` and tree-shakes out of the production runtime entirely. The worker pool is lazy, with no process forked until a probe actually fires, reused across probes for the same dev session, and torn down on HMR or worker crash. A snapshot of the outer request store is forwarded to the worker so that cache functions — including private caches that read `cookies()`, `headers()`, or `draftMode()` — see the same values they would in a real invocation.github.com-vercel-next.js · 88368254 · 2026-05-06
- 1.0ETVHonor Suspense-above-body opt-in for dynamic `generateViewport` (#93759)github.com-vercel-next.js · 3cf7aa24 · 2026-05-12
- 0.9ETVImprove deduping of concurrent `'use cache'` invocations (#91830) With #71286, we implemented deduping of cache entries under certain circumstances. For example, the following constructed example was fixed with the PR: ```js async function getCachedRandom() { 'use cache' return Math.random() } const rand1 = await getCachedRandom() const rand2 = await getCachedRandom() assert(rand1 === rand2) ``` However, this implementation relied on awaiting the two calls sequentially. When rendering components, this can usually not be guaranteed. E.g. the following example was not properly deduped: ```jsx async function Cached() { 'use cache' return <p>{Math.random()}</p> } export default function Page() { return ( <> <Cached /> <Cached /> </> ) } ``` This did render the same value, but only because we triggered two render passes, and the last cached value was used for both elements in the final render pass. But during the first render pass, the `Cached` function was called twice, and the cache entry was also set twice. With #75786, we also fixed the render scenario by wrapping the cached function in `React.cache`. This however did not work for route handlers. E.g. the first example rewritten as follows, and used in a route handler, still wouldn't be deduped: ```js const [rand1, rand2] = await Promise.all([ getCachedRandom(), getCachedRandom(), ]) ``` Furthermore, with this solution, nested cached functions could not be deduped across different outer cache scopes. This is because each cache scope creates its own `React.cache` scope. Example: ```jsx async function Inner() { 'use cache' return <p>{Math.random()}</p> } async function Outer1() { 'use cache' return <Inner /> } async function Outer2() { 'use cache' return <Inner /> } export default function Page() { return ( <> <Outer1 /> <Outer2 /> </> ) } ``` This PR introduces two-layer invocation tracking that deduplicates these cases without changing how cache handlers are implemented. Only the first invocation (the "leader") performs the cache handler lookup and generation. Subsequent invocations ("joiners") tee the leader's result stream instead. The deduplication scope starts after the Resume Data Cache (RDC) lookup and before the cache handler `get`. The RDC phase is excluded because it throws synchronous errors (dynamic usage errors) that need individual stack traces per call site, and RDC lookups are local with no network savings from deduplication. Intra-request deduplication is stored on the `WorkStore` and keyed by `serializedCacheKey` (the coarse key, which is safe because root params are identical within a single request). Cross-request deduplication is stored in a module-scope map keyed by `cacheHandlerKey`, which may include root params on the warm path. Cross-request joiners must await metadata for root param verification before forking the stream. If the key mismatches (different root params), the joiner retries with a recomputed key. Because metadata is checked before `fork()` is called, a mismatched joiner never consumes a stream from the wrong entry. Stream tee-ing is lazy via the `SharedCacheEntry` class: the leader calls `fork()` to get its copy, and each joiner calls `fork()` on demand. If no joiners exist, only one tee occurs. The `SharedCacheResult` discriminated union wraps either a `SharedCacheEntry` (for the cached case) or a hanging promise (for `prerender-dynamic`). Each invocation still decodes the stream independently via `createFromReadableStream` with its own `temporaryReferences`, because sharing the decoded result would cause cache poisoning when components receive different non-serializable props (e.g. `children`). Admittedly, this adds significant complexity to the `cache()` function. Two classes help keep it manageable: - `SharedCacheEntry` encapsulates stream ownership and lazy tee-ing so that call sites don't need to reason about which streams have been consumed or need cloning. - `ResolvableSharedCacheResult` manages the deferred promise, map registration, and lazy cleanup. Entries stay in the dedup maps until collection completes (so late-arriving invocations can join while the leader streams), then clean up automatically on resolve or reject. A follow-up refactoring to extract the cache handler lookup and generation into a separate function would help break up the function's size. closes #78703github.com-vercel-next.js · 1066fbf7 · 2026-04-17
- 0.7ETVSupport accessing root params in `"use cache"` functions (#91191) 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).github.com-vercel-next.js · 8283b126 · 2026-03-17
- 0.7ETVRestore dev-mode cache-fill timeout for `'use cache'` (#93055) When `'use cache'` fills during `next dev`, there was no timeout, so a stalled fill could hang the request indefinitely. This change restores a dev-mode cache-fill timeout that fires 50 seconds after the fill starts, matching the 50s timeout we already have during prerender. The timer sets `workStore.invalidDynamicUsageError` to a `UseCacheTimeoutError` and aborts the signal passed to React's `renderToReadableStream`, which errors the cache stream. From there the existing handling of `invalidDynamicUsageError` in app-render surfaces the error in the Redbox and the CLI output without any additional plumbing. Prior to PR #85747, `spawnDynamicValidationInDev` reused the prerender store and inherited the prerender cache timeout as a side effect. After that change the validation became staged-rendering based and the timeout stopped applying to dev renders. This PR closes that gap. The timeout is skipped when the render is already in the Dynamic stage, which mirrors prerender, where caches past `await connection()` aren't executed at all. The check is written as exact equality on `RenderStage.Dynamic` rather than a numeric `< Dynamic` comparison because `RenderStage.Abandoned` is numerically higher than Dynamic but semantically represents an aborted initial prospective render whose pending caches still need to be timed out while the outer flow awaits `cacheSignal.cacheReady()`. The 50s duration is shared with the prerender path via a new `USE_CACHE_FILL_TIMEOUT_MS` constant and carries a TODO to derive it from the user-configurable `staticPageGenerationTimeout`. The tests model a realistic migration hazard: a module-scoped in-flight fetch dedupe loader (the hand-rolled variant of the documented `React.cache`-based preload pattern) joined by a `'use cache'` function. That configuration deadlocks because the outer fetch is parked on the Dynamic stage in dev, or returns an intentionally hanging promise during prerender, and the cache never fills.github.com-vercel-next.js · 058de279 · 2026-04-21
- 0.6ETVCached Navigations: Cache visited fully static pages in the segment cache (#90306) When a fully static page is loaded (via initial HTML or client-side navigation), the segments are now written into the segment cache so subsequent navigations can be served entirely from cache without server requests. Initial HTML: The RSC payload inlined in the HTML is written during hydration via `getStaleAt` + `writeStaticStageResponseIntoCache` with `isResponsePartial: false`, using `FetchStrategy.Full` since all segments are present. A `StaleTimeIterable` (`s` field) is included in the `InitialRSCPayload` during the Cache Components prerender path to provide the stale time. Navigation: The `isResponsePartial` flag from the prepended byte (stripped by `processFetch`) is now also used to determine how segments from `writeStaticStageResponseIntoCache` are cached. For fully static routes (`isResponsePartial: false`), segments are written as non-partial so no dynamic follow-up is needed. The route tree used for cache writes is now always derived from the Flight data itself (via `convertServerPatchToFullTree`), rather than from a pre-existing route cache entry. This guarantees the tree stays in sync with the response and avoids key mismatches between the static stage tree and the per-segment prefetch tree for parallel route slots. When writing the head into the cache, `isHeadPartial` from the server is overridden to `false` for non-partial responses, but only when Cache Components is enabled. This corrects the server's conservative `isPossiblyPartialHead` marking during static generation. Without Cache Components, the server already sends the correct value. Responses without a recognized marker byte (e.g. regular navigations that don't go through the prerender path) are conservatively treated as partial. Partially static pages (where the static stage needs to be extracted via byte-level truncation of the initial HTML Flight stream) will be handled in a follow-up. --------- Co-authored-by: Andrew Clark <git@andrewclark.io>github.com-vercel-next.js · 1d14e93c · 2026-03-04
- 0.6ETVSupport accessing root params in `generateStaticParams` (#91189) A new `GenerateStaticParamsStore` work unit store type is now provided during `generateStaticParams` execution. This enables root param getters (`import { lang } from 'next/root-params'`) to be called inside `generateStaticParams`, allowing shared helpers that internally access root params via the special import to be used in both Server Components and `generateStaticParams` without manually threading params. Each `generateStaticParams` call now runs within a `workUnitAsyncStorage.run()` context carrying a `GenerateStaticParamsStore` with the correct `rootParams` (extracted from `parentParams` using the already-available `rootParamKeys`). The store extends `CommonWorkUnitStore`, providing `phase` and `implicitTags` which are not strictly necessary but convenient to keep call sites simple. Request-time APIs (`headers()`, `cookies()`, `connection()`, `draftMode()`) now throw specific errors when called inside `generateStaticParams` instead of the previous generic "called outside a request scope" message. Framework-internal functions like `createSearchParamsFromClient` and `createParamsFromClient` throw `InvariantError` since they should never be reached in this context. This change also unblocks a follow-up PR that removes `| undefined` from `PublicCacheContext.outerWorkUnitStore` in the use cache wrapper, since `"use cache"` is already supported inside `generateStaticParams` today but previously ran without a `WorkUnitStore`. With this store in place, requiring a `WorkUnitStore` in `"use cache"` won't break that existing usage.github.com-vercel-next.js · 4f693363 · 2026-03-16
- 0.5ETVHonor the route-level `expire` value with blocking revalidation (#93211) A prerendered route's `expire` — set via `cacheLife({ expire })` inside `'use cache'` or via the `expireTime` config fallback — lands in the prerender manifest as `initialExpireSeconds` / `fallbackExpire` (#76207), but the runtime never read it: `IncrementalCache.get` only considered `revalidate`. So past expire, Next.js served stale with a background refresh instead of the blocking regeneration the `cacheLife` `expire` docs describe. The fix is three coordinated changes. The render-time `responseGenerator` in `app-page.ts`, `app-route.ts`, and `pages-handler.ts` now applies the `expireTime` fallback as soon as it has the render's `cacheControl`, so every downstream consumer (the cache stored via `IncrementalCache.set`, the response `Cache-Control` header, the entry returned to `handleResponse`) sees a finalized `cacheControl` with a populated `expire` — mirroring the build-time fallback. `IncrementalCache.get` then returns `isStale = -1` when `lastModified + expire * 1000 < now`, and `response-cache.handleGet` skips its early `resolve(previousEntry)` for `isStale === -1` so the blocking revalidation inside `responseGenerator` (which already picks `BLOCKING_STATIC_RENDER` on that signal) can return its fresh output to the user. Previously the early resolve committed the stale value to the response first, so even though `responseGenerator` still ran a fresh render its output only warmed the cache for the next request. As a side effect this also closes the same early-resolve hole on the existing tag-expired `isStale = -1` path. On Vercel, ISR cache decisions live at the Proxy and the Proxy currently ignores `staleExpiration` (using a hard-coded one-year value instead). It is also expected, once it starts honoring `staleExpiration`, to pick up updated values from the `stale-while-revalidate` response header. Until that lands this change is only observable on `next start` — deploy-mode behavior is tracked independently of Next.js. Two test suites cover the new behavior. `test/production/app-dir/use-cache-expire` uses `cacheComponents` + `cacheLife({ expire: 300 })` with a custom cache handler that shifts `lastModified` via an `x-test-cache-age-offset-ms` header, exercising the fully-static shell, the partially-static route shell for a known param, and the partially-static fallback shell for unknown params. `test/e2e/app-dir/expire-time` covers classic ISR (`revalidate = 1`, `expireTime: 2`) with a real three-second wait and is `it.failing` on deploy, so it will flip the moment the Proxy honors the expire value. fixes #78269github.com-vercel-next.js · 8e4cfc50 · 2026-04-29
- 0.5ETVEncode non-ASCII characters in cache tags at construction (#93601) When a cache tag contains a non-ASCII character (Hebrew, CJK, emoji, …) it gets written into the internal `x-next-cache-tags` HTTP header on ISR responses. Node's `validateHeaderValue` rejects any byte outside `\t\x20-\x7e`, so the response crashes with `ERR_INVALID_CHAR`. On Vercel deploys stale-if-error masks the 500 from clients, but revalidation itself keeps failing and the cache stops refreshing for affected routes. This change introduces a single `encodeCacheTag` helper and applies it at every public boundary — `validateTags` (which `cacheTag()`, `unstable_cache()`, and `fetch` tags all funnel through), `getImplicitTags` for path-derived tags, and `revalidatePath` / `revalidateTag` / `updateTag` for invalidation inputs. The encoder matches runs of out-of-class code units so surrogate pairs reach `encodeURIComponent` intact, and it is idempotent on already-encoded `%xx` sequences, so callers can pass either the raw or the encoded form interchangeably. PR #93139 already encodes path-derived tags at construction, but it misses every user-supplied tag entry point and uses a `decodeURIComponent` round-trip that silently mangles literal `%xx` characters in tag values. PR #93167 encodes only at the `setHeader` sites, which leaves storage and invalidation diverging and requires every new write site to remember the encoding step. The canonical-form-at-the-boundary approach taken here covers all entry points and keeps storage, comparison, and the wire in sync. fixes #93142 closes #93139 closes #93167 Co-authored-by: Swarnava Sengupta <swarnava.sengupta@vercel.com> Co-authored-by: Or Nakash <ornakash@gmail.com>github.com-vercel-next.js · 9e183033 · 2026-05-07
- 0.5ETVFix inconsistent cache life/tags propagation for cache handler hits (#91454) When a `"use cache"` entry is newly generated during a prerender (`prerender` or `prerender-runtime`), `collectResult` defers propagation of cache life and tags to the outer context. This is because the entry might later be omitted from the final prerender due to short expire or stale times, and omitted entries should not affect the prerender's cache life. However, when a cache handler returns a hit for an existing entry, `propagateCacheEntryMetadata` was called unconditionally, without the same deferral logic. This meant that short-lived cache entries retrieved from the cache handler could propagate their cache life to the prerender store, even though they would later be omitted from the final render. This inconsistency is currently not observable because runtime prefetches use a prospective and final two-store architecture (see `prospectiveRuntimeServerPrerender` and `finalRuntimeServerPrerender` in `app-render.tsx`). The cache handler hit propagation corrupts the prospective store, but the response is produced from the final store, which reads from the resume data cache with correct stale and expire checks. Static prerenders have a similar two-phase architecture that masks the issue. Because of this, there is no test case that can observe the incorrect behavior, but the fix avoids confusion and prevents the inconsistency from becoming a real bug if the architecture changes. This change extracts a `maybePropagateCacheEntryMetadata` function that encapsulates the conditional propagation logic and is now called from both the generation path (inside `collectResult`) and the cache handler hit path. The resume data cache read path continues to call `propagateCacheEntryMetadata` unconditionally, since it runs in the final render phase after short-lived entries have already been filtered out.github.com-vercel-next.js · 23fa2787 · 2026-03-17
- 0.4ETVMake `'use cache'` fill timeout configurable (#93070) Applications can now override the `'use cache'` fill timeout through a new `experimental.useCacheTimeout` config (in seconds, matching the `staticPageGenerationTimeout` / `cacheLife` configs), which lets them raise the timeout for slower dev-time backends or lower it for faster feedback while iterating. Without a value set, the timeout defaults to 90% of `staticPageGenerationTimeout`, landing at 54s against the 60s default build timeout — slightly higher than the previous hard-coded 50s. During static and runtime prerendering, the effective timeout is clamped to `0.9 × staticPageGenerationTimeout` so the cache-fill error surfaces before the build worker kills the page. In dev mode, the configured value is used as-is since there's no equivalent build-worker kill. The defaulting and clamp are centralized in a single `getUseCacheFillTimeoutMs` helper in `use-cache-wrapper.ts`, replacing the previous module-level `USE_CACHE_FILL_TIMEOUT_MS` constant. The new value is threaded through `RenderOpts` and `WorkStore`. Proxy/middleware and the edge route-module wrapper construct a WorkStore but never fill `'use cache'` entries, so they pass `0` sentinels with a comment — if anything ever reads these values, the cache will time out immediately and surface the bug loudly. We'll revisit this if we ever add `'use cache'` support to Proxy. A new `use-cache-configured-timeout` fixture exercises the clamp by setting `staticPageGenerationTimeout: 10` and flipping `useCacheTimeout` between `15` (in dev, via `process.env.__NEXT_DEV_SERVER`) and `60` (in build). Two pages, one sleeping 11s (between the 9s clamp and the 15s dev timeout) and one sleeping 17s (above the dev timeout), let us assert that the dev value is applied without clamping and that the build clamps both pages. The existing `use-cache-hanging` fixture now sets `useCacheTimeout: 10` so tests don't race the browser's `page.goto` ceiling, and the explicit 180s test timeouts there have been dropped in favor of the Jest default.github.com-vercel-next.js · 8de4b600 · 2026-04-21
- 0.4ETVImprove error stacks for dynamic API usage in `"use cache"` (#92736) When `cookies()`, `headers()`, `draftMode()`, or `connection()` are called inside `"use cache"` from third-party (ignore-listed) code, the error stacks previously had no usable frames. The redbox showed no source location and an empty call stack, and the build output only showed `at ignore-listed frames`. This made it difficult to trace which component was responsible for the invalid API usage. This PR fixes the issue by calling React's `captureOwnerStack()` at the `"use cache"` boundary (in `use-cache-wrapper.ts`) before entering the cache scope, and storing the result on the cache work unit store as `outerOwnerStack`. When an error is later thrown inside the cache scope (e.g. from `cookies()`), `applyOwnerStack()` concatenates the inner owner stack (from within the cache scope) with the stored outer owner stack to reconstruct the full component tree across cache boundaries. This also correctly handles nested `"use cache"` scopes by chaining the outer owner stacks. The `applyOwnerStack` function was extracted from `io-utils.tsx` into `dynamic-rendering-utils.ts` so it can be reused by both the request API files (`cookies.ts`, `headers.ts`, `draft-mode.ts`, `connection.ts`) and the sync IO error handling in `io-utils.tsx`. New test cases cover both first-party and third-party usage of all four APIs, including a previously missing `connection()` in `"use cache"` test case.github.com-vercel-next.js · 6541ab5b · 2026-04-14
- 0.4ETV[Flight] Fix stranded row content under Node stream backpressure (#36516) The Flight Server emits Text and TypedArray rows as two chunks: a header that gives the row's id, type, and content length, followed by the content itself. These two chunks were pushed into `completedRegularChunks` (and `completedDebugChunks` in DEV) as separate items, so when the destination signaled backpressure between them, the flush would write the header and then break out before reaching the content. The content chunk was left stranded at the head of the queue. Async work running while the destination was paused appended new rows to `completedImportChunks` / `completedHintChunks`, and the next drain flushed those queues first — splicing the newly-arrived bytes into the position the Flight Client expects to read as the original row's content. From there the Flight Client read rows from the wrong byte offsets and the model failed to deserialize. This only surfaced on the Node stream path. `createFakeWritableFromReadableStreamController`, used by `renderToReadableStream`, always returns `true` from `write()`, so the flush loop never saw backpressure. The fix pushes a `NEXT_TWO_CHUNKS_ARE_ATOMIC` sentinel ahead of each `headerChunk` / `contentChunk` pair in `completedRegularChunks` and `completedDebugChunks`. The flush loops detect the sentinel and write the two chunks that follow it together before re-checking backpressure, so backpressure can still break between rows but never within one. ### Alternatives considered - **Pushing the pair as a `[headerChunk, contentChunk]` tuple.** Simpler but allocates an array per row. The required `isArray` branch in the flush hot path is likely comparable with the symbol check. This also violates the opaque `Chunk` type boundary. - **Concatenating header and content into one chunk.** Bad for memory — typed-array content can be large. - **Storing atomic groups in a separate queue.** Conceptually wrong and risks breaking reference-ordering assumptions between rows. - **Ignoring backpressure until the regulars queue is empty.** Defeats the point of backpressure. - **Wrapping the tuple behind a host-config API** (`writeAdjacentChunks` / `isAdjacentChunks` / `chunksToAdjacentChunks` / `getAdjacentChunksLength`). Keeps the implementation opaque but adds four exports per host config. Also has the tuple overhead. - **Begin/end sentinels for variable-length atomic groups.** Not needed — only Text and TypedArray rows use this pattern, and both are pairs.github.com-facebook-react · c0148134 · 2026-05-27
- 0.4ETVCached Navigations: Cache runtime stage data from navigation requests (#90666) When navigating to a page with `unstable_instant: { prefetch: 'runtime' }` and no prior prefetch (e.g. `prefetch={false}` or clicking before prefetch completes), the server now embeds a runtime prefetch stream in the dynamic RSC navigation payload. On the client, this stream is decoded and written into the segment cache so that subsequent navigations to the same page can serve runtime-prefetchable content (cookies, headers, searchParams) instantly, without needing a separate prefetch request. On the server, `generateStagedDynamicFlightRenderResult` detects routes with runtime prefetching enabled and attaches a `CacheSignal` and `PrerenderResumeDataCache` to the `RequestStore`. The dynamic render fills these caches as a side effect. Once `cacheSignal.cacheReady()` resolves, a final runtime prerender (the same 5-stage pipeline used for regular runtime prefetches) runs and its output is piped into a `TransformStream` whose readable side is included in the RSC payload as the `p` field on the `NavigationFlightResponse`. This avoids a separate prospective prerender because the dynamic render has already warmed the caches. On the client, `processRuntimePrefetchStream` strips the isPartial byte, decodes the inner Flight stream, and the result is written into the segment cache via `writeDynamicRenderResponseIntoCache` with `FetchStrategy.PPRRuntime`. Both the `navigateToUnknownRoute` and `fetchMissingDynamicData` code paths handle the new stream. This is gated on the explicit `unstable_instant` opt-in because it adds extra server processing (a full runtime prerender per navigation request) and increases payload size. It also requires that the runtime prefetch output has been validated first. Known limitation: the runtime prefetch cache write currently evicts the static cache entry (which may have a longer stale time) because `upsertSegmentEntry` treats the fallback match as a replacement target. This means that after the runtime cache expires, the static cache is no longer available as a fallback. A follow-up may address this with a layered cache approach where entries with different fetch strategies and stale times coexist independently. Extracting runtime data from initial HTML loads (the PPR resume path) is also deferred to a follow-up.github.com-vercel-next.js · 4b56af9f · 2026-03-04
- 0.3ETVCached Navigations: Cache static stage of partially static initial HTML (#90539) When a partially static page is loaded via initial HTML (PPR resume), the RSC payload now includes the `l` (static stage byte length) field, enabling the client router to extract and cache the static stage during hydration. Subsequent navigations to the same page serve the cached static content instantly while streaming in dynamic content. On the server, the resume path in `renderToStream` now uses staged rendering (mirroring `generateStagedDynamicFlightRenderResult`) when Cache Components is enabled. A `StagedRenderingController` separates the static and dynamic stages, and `countStaticStageBytes` resolves the byte length promise that Flight serializes into the stream as `l`. `getRSCPayload` accepts `staleTimeIterable` and `staticStageByteLengthPromise` options to wire `s` and `l` into the `InitialRSCPayload`. On the client, `app-index.tsx` tees the inlined Flight stream when Cache Components is enabled. One copy goes to React for decoding, the other is passed to `createInitialRouterState`. When `l` is present (partially static), the clone is truncated at the byte boundary via `decodeStaticStage` and written into the segment cache via `writeStaticStageResponseIntoCache` with `isResponsePartial: true`. When `l` is absent but `s` is present (fully static), the same function is used with `isResponsePartial: false`. `createInitialRouterState` now accepts a single `initialRSCPayload: InitialRSCPayload` prop instead of individual destructured fields.github.com-vercel-next.js · 5a18a71b · 2026-03-04
- 0.3ETVEnable `varyParams` tracking for Cached Navigations (#92113)github.com-vercel-next.js · a215ea60 · 2026-03-31
- 0.3ETVRevert "Prerender HTTP access fallbacks with Cache Components semantics" (#94018) Reverts vercel/next.js#93988 This broke not-found handling for internal Vercel apps.github.com-vercel-next.js · 9741cb57 · 2026-05-21
- 0.3ETVWarn for non-deterministic `"use cache"` args during final prerender (#92820) 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. 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 `dynamicCacheKeys` set 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.github.com-vercel-next.js · 662c6d57 · 2026-04-15