Janka Uryga
90d · built 2026-09-30
Performance
What Janka Uryga shipped in the selected window, measured in ETV, and how it compares with the 90 days before it.
Effective capacity
+1.9engineers
delivers like 2.9 (2.9x pre-AI)
Output (ETV)
18.2ETV
+84.8% vs 9.8 prior
Features share
10.4%
−11.7 pp vs prior window
Fixes share
12.7%
−9.1 pp vs prior window
Work mix
10.4% Features9% Maintenance66.6% Tests1.3% Docs12.7% Fixes
42 commits over 90 days, ending 2026-09-30.
Daily performance
Daily ETV, stacked by Features, Maintenance, Tests, Docs and Fixes.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
Most impactful commits
Top 10 by ETV in the last 90 days.
- 2.3ETV[PPF] Separate tracking of runtime data accesses for shells and prefetches (#98129) Expands the static tracking from #96095 to track shells and prefetches separately. We split the `ShouldAttemptStaticPrefetch` hint into `ShouldAttemptStaticShell` and `ShouldAttemptStaticPrefetch`, which the client router considers separately depending on whether we need a shell or a prefetch. The server-side tracking of `needsRuntimeRequest` becomes a bit more subtle. First, a runtime data access should only affect the shell if we're in the `ShellStatic` render stage. If we're already in the `PrefetchStatic` stage, then that content is not conceptually part of the shell, and should not deopt the shell to runtime, so we only unset `shouldAttemptStaticShell` if we're still in the shell stage. Second, accessing data that would only resolve in the prefetch (params, search params, short-stale caches) should **not** affect whether the shell can be static. `makeRuntimeDataHangingPromise` has been split into variants that match this: - `makeSessionDataHangingPromise` - data that affects the shell (and the prefetch), e.g. `cookies()` - `makeURLDataHangingPromise` - data that only affects the prefetch, e.g. `params` - `makeUnknownRuntimeDataHangingPromise`, used for private caches -- we omit these during build so we don't know whether they should affect the shell or only the prefetch, so we conservatively affect both (unless a private cache is only used in the prefetch stage) The server-side `needsRuntimeRequest` promise is now resolved such that data that only resolves in the prefetch does not affect the shell. If we're accessing data that only resolves in the prefetch, we will delay the promise's resolution to the `PrefetchStatic` stage, which means that when the client rewinds the static response to a shell, it will see an unresolved promise and consider the response as not having used runtime data. (see `markRuntimeDataAccessWhenStageReached`)github.com-vercel-next.js · e1131ee7 · 2026-09-22
- 2.1ETV[PPF] `ensureStatic = "navigation"` (#99171) Closes NAR-930 Initial implementation of `ensureStatic = "navigation"` ### Behavior This version of `ensureStatic = "navigation"` makes all prerenders blocking. All params must be specified via `generateStaticParams`, similar to `output: "export"`. This is meant to ensure that we don't serve a fallback + a resume for a param that wasn't prerendered, which would violate the expectation that `ensureStatic = "navigation"` means no per-request compute, which is what we're trying to provide. Note that Client-only IO (like `use(io())` or `useSearchParams()`) is allowed, because it doesn't require a resume render -- we serve HTML with fallbacks and render the missing content in the browser. (this is existing behavior and not changed here) Earlier versions of this PR attempted to allow fallback shells if they managed to produce a complete RSC result, e.g. on a route with params that are only used via `useParams()`, where could use the same browser bailout trick as other client IO. This proved complex to implement correctly, so we'll revisit that in the future ### Prerender output changes We skip producing all fallback shells except the most generic one in `buildAppStaticPaths`. Initially, i wanted to remove all of them, but it seems like the deploy output is incorrect if we don't have segment data from a fallback prerender. We also error for incomplete/missing `generateStaticParams`, which makes it impossible to create a purely-fallback param. ### Validation This PR extends the Static Shell Validation logic to be more restrictive when `ensureStatic = "navigation"`. We can't allow _any_ usages of dynamic data, even when wrapped with suspense. The new logic is expressed in `trackDynamicAccessInStaticRoute` + `getDisallowedReasonsInStaticRoute` which we use both in dev and prod. we check whether the server prerender was partial (which is already an error) and then run static shell validation so we can report where the hole was. This has some drawbacks: - if we have both server and client holes we might misreport the client ones as dynamic data. I have some ideas for how to address this, but for now this is the best we can do. (see `mixed server and client holes` in `ensure-static-navigation.test.ts` for examples) - if the client got a dynamic data promise but didn't `use()` it during the prerender (maybe because it was just seeding a cache for use in the browser), we won't have a dynamic hole to point to. in that case we'll error with a generic error message instead (see `errorPattern.body.withoutLocation` in `ensure-static-navigation.test.ts`). i've added new error messages to match the constraints of `ensureStatic = "navigation"`. we no longer recommend `instant = false` or `suspense` for dynamic data, because that's not a valid fix. there's a new suggestion: "make your \`params\` static via `generateStaticParams\`". (note that the messages don't have the "pretty" errors with suggestion cards yet, just the string message. i will update the dev error logic in a follow-up. docs are needed as well) i've also done a bit of cleanup: `trackDynamicHoleInStaticShell` and `trackDynamicHoleInRuntimeShell` are now one function that takes a `kind` parameter, because they only differed on the error message variant they reported (runtime vs dynamic).github.com-vercel-next.js · abe3b50d · 2026-09-28
- 1.2ETV[Instant] Point to first blocking await in validation (#96343) Closes NAR-854 ### TLDR New approach to #94949. We can work around the problem of debug info getting clamped when re-serializing by doing more work, which should be good enough unless we can figure out the correct way to adjust the clamping behavior in react. ### The problem Before this PR, instant validation works like this: 1. deserialize the full page payload (in stages) 2. reserialize each segment (in stages) 3. reassemble the segments into a combined payload, varying the stage per segment as needed Unfortunately, in step 2, we lose timing debug info that tells us when IO operations started/ended: https://github.com/facebook/react/blob/c235eb40f1c3121798d233bf992fd8f8c8558d06/packages/react-server/src/ReactFlightServer.js#L5550-L5553 This means that even if we provide an appropriate `endTime` during step 3, no IO info will be omitted by `filterDebugInfo`, because it was all re-timed to when the render occurred. In practice, this means that when we report a blocking dynamic hole, it might point to a different await than the one that was actually blocking in the stage that we rendered the segment in. ### The solution (h/t to @gnoff for coming up with this trick) We can work around this problem with no changes in React. We just have to apply an `endTime` when deserializing the payload for the first time, before reserializing each segments. However, this means that we can no longer disassemble the payload into segments in one pass -- e.g. if we trim the debug info to what it'd be in the `Static` stage, we can't get good data for the `Runtime` stage anymore. So we have to run the segment disassembly separately for each stage we care about. The debugInfo can now vary depending on the stage, we need to change the shape of `SegmentCacheItem`. Previously, it was like this: ``` '/foo': chunks: Static: [...] Runtime: [...] Dynamic: [...] debugChunks: [...] ``` But to account for the varying debug info, we now need to store the partial chunks, the complete chunks, and the debug chunks separately for each stage: ``` '/foo': Static: chunks: [...] allChunks: [...] debugChunks: [...] Runtime: chunks: [...] allChunks: [...] debugChunks: [...] Dynamic: ... ``` Most of the changes in the PR come from adjusting the code to - create separate entries for each segmentPath + stage in the new way outlined above, using an `endTime` based on the stage - consume the cache entry appropriatelygithub.com-vercel-next.js · 8c609c3e · 2026-07-30
- 1.0ETV[PPF] unstable_navigation() (#96908) `navigation()` is a new API that allows omitting contents from runtime shells and runtime prefetches. Conceptually, the point is to express that something is expensive to compute, so we shouldn't do it for requests that may not get used (shells and prefetches). Notably, this means that it's fine to include it in a static prerender -- it'll be computed once and used for many requests, so it doesn't make sense to exclude it. ## Implementation The split in behavior across static and runtime prerenders is a departure from how most of our APIs behave -- usually, if something resolves statically, then it also resolves in "more complete" prerender. Departing from this leads to some implementation complexity. We include three new stages, used by two facets of the implementation: ```diff export enum RenderStage { Before = 1, // ShellStatic = 10, + PrefetchStatic = 11, <------- params, prefetch() [static prerenders] + NavigationStatic = 12 <------navigation() [static prerenders] Static = 13, <--------------- finish accumulators [static prerenders] // ShellRuntime = 20, Runtime = 21, <-------------- params, prefetch() [runtime prerenders] + NavigationRuntime = 22, <---- navigation() [runtime prerenders] // Dynamic = 30, Abandoned = 40, } ``` ### NavigationRuntime In runtime prerenders (or dev renders that simulate them), `navigation()` resolves in `NavigationRuntime` We only reach this stage in 1. the embedded runtime prerender produced for Cached Navigations and 2. during dev/prod full staged renders -- runtime shells end in `ShellRuntime`, and runtime prefetches end in `Runtime`. Notably, this means that content gated behind `navigation()` is included in the embedded runtime prefetch stream. ### PrefetchStatic & NavigationStatic This is a helper stage added before `Static`. Static prefetches still use the `Static` stage for their output. This new stage exists so that we can resolve static `params` (and `prefetch()` when we implement it) which the stage is named after) separately from `navigation()`, which resolves in `NavigationStatic`, after which the prerender ends in `Static`. This separation is important, because during static prerenders we track whether or not runtime APIs are used (see `trackRuntimeDataAccessed`) to determine if a runtime shell (or runtime prefetch) might give us more content than the static ones. However, a runtime shell/prefethc **would not resolve navigation()**, so `await navigation(); await cookies()` would not reveal more content, and thus shouldn't count as a usage that prevents static optimization. We achieve this by checking the stage inside `trackRuntimeDataAccessedImpl` and not tracking anything if we reached the `NavigationStatic` stage. ### Behavior of shells and validation As noted before, `navigation()` has an incompatible resolution order between static and runtime prerenders. In #97040, we did some groundwork to deal with this in validation. Static prerenders resolve `navigation()`, which means that static shells include content gated behind navigation(). This means that Static Shell Validation allows them. On the other hand, App shells **do not** resolve `navigation()`. This leads to an inconsistency for Instant Validation -- a `await navigation()` might be fine if a page is prefetched statically, but would become blocking as soon as the page starts using runtime data and switches to a runtime shell. To avoid this pitfall, we pessimistically assume that any `navigation()` _might_ be part of a runtime shell/prefetch, so any `navigation()` unguarded by Suspense will error in IV. In practice, this is handled analogously to static params: we do a dev render with `needsAppShell: true`, which makes `navigation()` resolve in `NavigationRuntime`, and then we use the `ShellRuntime` stage when validating, which means that `navigation()` will be a hole. Note that the discriminated error message logic currently only retries errors using the `Runtime` stage, which won't have `navigation()` resolved either, so it will be incorrectly reported as dynamic data. This will be improved in a follow up.github.com-vercel-next.js · 1be0ab80 · 2026-08-20
- 0.9ETV[PPF] Validate `unstable_ensureStatic` nesting (#98357) `ensureStatic` forms a hierarchy: `"auto" < "shell" < "prefetch" < "navigation"`, with each value being more contrained than its predecessor. 1. A child segment cannot relax constraints set by a parent segment, it can only tighten them - so `shell` in a layout and `prefetch` in a page is fine, but the opposite is not - note that setting `"auto"` in the child *is* allowed, because it means "unconstrained" and is compatible with all the other configs 2. We can't have two conflicting levels set by two parallel slots (but `auto` is compatible with any other constraint, so it's allowed, same as above) 3. If a segment sets `ensureStatic = false`, then no other segments on that route are allowed to constrain the route by setting anything higher than `auto` (this is meant as a signal for e.g. "this segment DELIBERATELY uses a runtime shell and should not be overridden to be static") Also adds exhaustive tests for all of these scenarios.github.com-vercel-next.js · 690f1ad8 · 2026-09-23
- 0.8ETVInstant validation for caches excluded from prerenders (#98342) Follow-up to #98339. Turns out we were not testing how caches excluded from static/runtime prerenders and app shells behave in Instant Validation, and it was somewhat broken. This PR adds test coverage and fixes some bugs I found along the way. ### Fixes - `use-cache-wrapper` was incorrectly gating cache delays in "request" stores on `NODE_ENV === "development"`, which does not include build-time instant validation. The correct check is now implemented in `isValidationRender` - `use-cache-wrapper` has divergent behavior for caches with `stale < MIN_SHELL_STALE` across app shells and PPR/static shells, which needs to be tracked so that we know that the same render can't be used for both Instant and Static Shell validation. When we see a cache entry like that, we now call `trackIncompatibleShellContent()` - A render that had a cache miss could still report that it's compatible with both SSV and IV, because we only do the above for a cache hit. As a result so we'd incorrectly reuse `LAZY_FULL_RENDER` for both. Cache misses now result in a `trackIncompatibleShellContent()` call to avoid this ### Tests We now have tests for: - `stale < MIN_SHELL_STALE` - excluded from app shells, but included in static and runtime prefetches - `stale < MIN_PREFETCHABLE_STALE` - excluded from all prerenders - `expire < MIN_PRERENDERABLE_EXPIRE` - excluded from static prerenders, but allowed in runtime prerenders Due to bugs mentioned above, some of the added tests were failing before the fixes (mostly the ones that expect an error -- passing a "no validation errors" test is easy, just don't create any dynamic holes) In build, these tests incorrectly reported no errors when they should've failed IV: - `invalid - unguarded non-prefetchable cache (with short stale)` (both PPF and non-PPF) - `non-app shell validation > invalid - unguarded non-prerenderable cache with short expire` in build: - `app shell validation > invalid - unguarded cache with a shorter-than-shell staleTime` in build: This is because we were missing cache delays in build-time instant validation (now fixed with `isValidationRender`), so the caches weren't dynamic holes at all. The `stale < MIN_SHELL_STALE` tests (`app shell validation > invalid - unguarded cache with a shorter-than-shell staleTime`) were also failing in dev: - **{initial load, client navigation} with cold caches**: Should be an IV error, but is an SSV error. The initial render had cache misses, so we did a warm-cache full rerender with runtime shells, which resolved `await nonShellCache()` in the Runtime stage. But the initial render **did not track incompatible data**, so we incorrectly re-used it for SSV and IV. The cache was resolved in `Runtime` so SSV saw a runtime hole and `await nonShellCache()` errored in SSV with `Next.js encountered runtime data during prerendering.` - **client navigation with warm caches**: same as above, except there weren't cache misses, so incorrectly reused the *original* runtime-shell render for SSV and IV - **initial load with warm caches**: No redbox when IV should've errored. The main render was an initial load and did not use runtime shells, so it resolved `await nonShellCache()` in `PrefetchStatic`. We had no cache misses and **did not track incompatible data**, so we incorrectly re-used the main non-runtime-shell render for SSV and IV. The cache resolved in `PrefetchStatic` and it wasn't a hole in `ShellRuntime` so no error was reported.github.com-vercel-next.js · f4449e8f · 2026-09-08
- 0.8ETV[PPF] unstable_prefetch() (#97622) Implements `unstable_prefetch()`, which is intended for use in `partialPrefetching`. `await unstable_prefetch()` excludes content from the app shell -- it will only be available when using `prefetch={true}` (speculative prefetch) or during navigations. As a rule of thumb, `unstable_prefetch()` resolves whenever static `params` would: - in a static prerender - but NOT the app shell extracted from it, which is param-less - in a runtime prefetch (`prefetch={true}`) - but NOT a runtime app shell, which is param-less Note that `prefetch()` is URL data, so using it in an App Shell without Suspense will trigger an instant insight. ### Implementation notes `prefetch()` is treated like URL data, so it resolves in the `PrefetchStatic/PrefetchRuntime` stages added in #96908. The implementation is basically analogous to `unstable_navigation()` except using different stages. i've considered abstracting them into one implementation, but decided against that for now, we can deduplicate later. Error messages about URL data have not been updated to mention it yet -- we will do that as a follow up, along with docs. `await prefetch()` does not count as a runtime data access, meaning that it won't affect the static prefetch hint for a route. however `await prefetch(); await cookies()` does deopt the route, because using a speculative runtime prefetch would reveal more content. Note that this may cause us to unnecessarily deopt a shell to runtime even if only the speculative part of the content would be improved by a runtime request; this is not a new issue, but it's something we should optimize.github.com-vercel-next.js · 3cdb56c2 · 2026-08-21
- 0.7ETV[PPF] Fix unnecessary runtime prefetch with prefetch={true} (#98651) In, we #98278 attempted to fix runtime follow-ups for shells (if a static prefetch produced an insuffient shell). However, it introduced a regression in this scenario, where neither shells or prefetches use runtime data, so both of these links should use static prefetches: ```tsx <Link href="/static-param/one" prefetch={true} /> {/* Revealed later */} <Link href="/static-param/two" prefetch={true} /> ``` We'd correctly do a static prefetch for the first link. However, the second link would see a `RuntimeShell`-tier shell from the first link, and this check inside `isShellEntryEligibleForStaticAttempt` would fail (because `RuntimeShell` was now greater than `PPR`): https://github.com/vercel/next.js/blob/3c9d1ca77f7de845315dc166a57fc9f090c638b4/packages/next/src/client/components/segment-cache/scheduler.ts#L1261 as a result, `isShellEntryEligibleForStaticAttempt` would return `false`, and we would deopt to a runtime prefetch for the second link instead. Importantly, a page with `ensureStatic = "prefetch"`(which will be implemented in #98191) will currently be handled exactly like a page that did not use runtime data at build -- the client doesn't know that a page is meant to never use runtime requests at all, it's entirely based on the existing static hints/`needsRuntimeRequest` promise mechanism. So even if `/static/[slug]` sets `ensureStatic = "prefetch"`, we'd also do an unnecessary runtime prefetch for it even though we really shouldn't. Violating the `ensureStatic` contract seems worse than a missing runtime shell follow-up , because those can currently only happen if the shell starts using runtime data after a revalidation, and that's probably not a common scenario. So i'm partially reverting that change until we figure out how to handle this in a way that handles both cases correctly (i've attempted doing that, but it seems nontrivial) ### Testing We're now missing the runtime shell follow-up again, so tests related to that in `prefetch-app-shell-revalidation.test.ts` are marked as failing (but still assert on current behavior -- the follow-up failures can be reproduced by setting `REPRODUCE_MISSING_RUNTIME_SHELL_FOLLOW_UP=1`). Note that the runtime *prefetch* follow ups there work as expected -- those worked correctly even before #98278. I've also added some pretty comprehensive tests in `prefetch-static-shell.test.ts`. We now test the scenario that regressed, which we weren't doing before. We also test scenarios where we reveal multiple links, but some of them use runtime data in the prefetch, and some don't. There's some failures there related to the fact that we don't track runtime data accesses in shells and prefetches separately (revealed by `REPRODUCE_UNNECESSARY_RUNTIME_SHELL=1`), which will be addressed by #98129. I want to land that before doing anything else on this topic to avoid dealing with failures that are going to be fixed soon anyway. There's also one case (`REPRODUCE_UNNECESSARY_RUNTIME_PREFETCH=1`) which will not be fixed by it, and will need separate consideration.github.com-vercel-next.js · 99083281 · 2026-09-17
- 0.7ETV[PPF] Fix missing runtime-follow up if hints are stale (#98278) if a page didn't use runtime data during build but started doing so after the revalidation (i.e. the hints say its statically-prefetchable but now it's not), we're supposed to be doing a follow-up runtime request based on the `needsRuntimeRequest` promise (introduced in #96095), but this was not actually happening. the bug goes as follows: 1. if we get an entry from a static prefetch, but `needsRuntimeRequest` is true, we save it as `PPR` (or `StaticShell` if it's a shell rewound from the static response). otherwise, we'd have saved it as `PPRRuntime` (or `RuntimeShell` for shells) to indicate that it satisfies those requests. 2. `wouldRuntimeRequestProvideMore` compares the runtime strategy for the current static walk to the entry's strategy to check if it should do a runtime request. this boils down to a comparison of `RuntimeShell > PPR` for a shell request 3. however, in the `FetchStrategy` enum, `RuntimeShell = 2` and `PPR = 3`, so `RuntimeShell > PPR` is false, which makes `wouldRuntimeRequestProvideMore` return `false`. so, if we have a `PPR` entry, **we'll never do a follow-up runtime shell request**. This is fixed by swapping the order of the two values in the enum. Now, if we end up with a `PPR` response where `needsRuntimeRequest` is true due to runtime data, that indicates that we *should've done a runtime request in the first place*, we just didn't know we should do so because the prefetch hint was stale, so the new ordering achieves the correct behavior, i.e. `pingSegmentBundle` decides that it needs a runtime shell request, which makes it match the intended PPF prefetching behavior. The enum ordering is a bit of a lie, because there is no single correct ordering -- a `PPR` entry *might actually provide more content* than a `ShellRuntime` entry if the route uses static params. In practice this is resolved by the vary path (shell responses are keyed with fallback params, and a PPR entry with static params would have the actual param values, and the param-ful entry would end up being preferred) so the strategy order wouldn't matter. Now that `RuntimeShell` beats `PPR`, i had to disable the strategy upgrading (`StaticShell -> RuntimeShell` and `PPR -> PPRRuntime`) on routes that don't use Partial Prefetching -- otherwise, a rewound`ShellRuntime` entry from a different param was winning over `PPR` in `pingSegmentBundle`, so it was preventing issuing new static requests even if we needed to do them. However, without PPF, the runtime tiers are not meaningful anyway, so this should be fine. ### Other bugs This fix also revealed a bug where `FetchStrategy.Full` requests were writing a shell as `RuntimeShell` even though the byte offset only marks a *static* shell. I've refactored the logic for picking the strategies to fix this and make it more explicit. ### Edge cases there is one (somewhat involved) edge case to this that i know of. if the post-revalidation static response has: 1. a shell that didn't access runtime data (i.e. `needsRuntimeRequest = false` in the shell stage) 2. a prefetch that did access runtime data (i.e. `needsRuntimeRequest = true` in the static stage) then we'll: 1. write the shell as `ShellRuntime` (because `needsRuntimeRequest = false`, and a runtime shell would not in fact provide more data) 2. write the prefetch as `PPR` (and not `PPRRuntime`, because `needsRuntimeRequest = true`, and a runtime prefetch *would* provide more data) with the new ordering, this means we'll end up preferring the `ShellRuntime` entry over the `PPR` entry even though the shell is a strict subset of it and would be missing static params and content gated behind `prefetch()` or `navigation()`. (note that this only happens if they share the same vary path) this can be observed as follows: 1. when a `<Link prefetch={true}>` to such a page is revealed, we won't request a runtime shell (due to already having a `ShellRuntime` entry), but we will request a runtime prefetch (because `PPR` is not enough). so far this is correct 2. if the navigation happens **before** this runtime prefetch completes, we will display what we already have. but we'll prefer the rewound `ShellRuntime` shell entry over than the `PPR` entry it originated from because of the enum ordering. so we may end up not showing content despite having it in the cache i'm not sure how to resolve this yet, but i think the current `recordedFetchStrategy` trick can't handle this, and we need a more involved mechanism that can detect when the `PPR` entry is actually better. i don't consider this a blocker for this fix thoughgithub.com-vercel-next.js · b0b7a429 · 2026-09-08
- 0.7ETV[PPF] Fix navigation() in prospective runtime prerenders (#98000) ### Long story short Fixes an analogous bug to #95665, but affecting `prefetch()/navigation()`. Those two were accidentally always resolving in the **prospective** runtime prerender, so we were warming content that isn't needed for the final runtime prerender. We now guard them properly. Also, I replaced `PrerenderStoreModernRuntime.isSessionShell` with `finalStage` (the same stage we're gonna use for the final prerender), so we can include/exclude `navigation` properly -- the boolean only distinguished shell/prefetch, not navigation. ### Long story long We use runtime prerenders for three things: runtime shells, runtime prefetches, and embedding a prefetch in navigations. These are aborted after `ShellRuntime`, `Runtime`, and `NavigationRuntime` respectively (accessible in `stageController.finalStage`). If we're doing the **final** prerender for a shell, promises that would resolve in e.g. `NavigationRuntime` would never resolve, because we abort the prerender before we get there. However, the **prospective** runtime prerender has no `StagedRenderingController` and needs to model this separately (so that the same APIs hang in both). This was [previously done via `prerenderStore.isSessionShell`](https://github.com/vercel/next.js/pull/95665) which we checked in URL data apis i.e. `params/searchParams` and the metadata`pathname` to do this. `isSessionShell: true` meant they should hang because it's a shell, and `isSessionShell: false` meant they should resolve. However, I forgot about this case when implementing `navigation`/`prefetch`, and made them resolve unconditionally in the prospective prerender. This means we'd potentially warm content that would not actually be reached in the final prerender. In other words, ```ts await navigation() return <Expensive /> ``` would needlessly render `<Expensive />` during the prospective render but not in the final one. This kinda defeats `navigation`'s main purpose, i.e. avoiding expensive work. This PR fixes the bug by replacing `isSessionShell: boolean` with `finalStage: AdvanceableRenderStage`, which is set to the same value we put in `stageController.finalStage`. This lets us keep the logic of what hangs and what resolves relatively close in both prerenders. --- On a meta level, the fact that the same bug happened twice tells me that having a `StageRenderingController` that is sometimes `null` is error-prone, and we should figure out a better solution, but i'm gonna keep this fix targeted and figure out a more holistic solution in a follow-upgithub.com-vercel-next.js · d8f95c12 · 2026-08-28