Janka Uryga
90d · built 2026-09-09
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.5engineers
delivers like 2.5 (2.5x pre-AI)
Output (ETV)
15.8ETV
+266.8% vs 4.3 prior
Features share
13.0%
−9.0 pp vs prior window
Fixes share
20.7%
−4.8 pp vs prior window
Work mix
13% Features9.9% Maintenance55.2% Tests1.2% Docs20.7% Fixes
46 commits over 90 days, ending 2026-09-09.
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.
- 1.5ETV[PP] Validate Shell prefetches (except gSP) (#95151) Implements instant validation for `partialPrefetching`. In this mode, `<Link>` prefetches an App Shell, which cannot access link data, and we need to warn for that. The changes in `instant-validation.tsx` are relatively simple: for an App Shell, we simply use `ShellRuntime` for all the new segments. We might also force them into `Runtime` for the purposes of discriminating dynamic holes. If a hole is present in `ShellRuntime` but disappears in `Runtime`, then we know it's caused by **link data** (as opposed to runtime or dynamic data). I've added some new error messages for this case. Note that the implementation here is incomplete: it uses the chunks from the dev render, which resolves static params in the `Static` stage. We use `ShellRuntime` for validating the App Shell, so as a result, static params are incorrectly included in it and don't trigger link data errors. This will be implemented in a follow-up. Note: It seems like we have some pre-existing bug in build validation where `fallbackParams` aren't populated, so params resolve statically when they shouldn't. I've marked two tests with `// TODO(app-shells): missing fallback params in build validation` so we can follow up and fix those.github.com-vercel-next.js · c131314b · 2026-07-01
- 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.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.7ETVfix: request APIs in promises passed to after() in actions/handlers (#94964) Fixes a bug where Route Handlers and Server Actions were prevented from calling `headers()` (and other request APIs) if - the call happened after the response is finished - the call *did not happen* inside an after-callback generally, this happens when you pass a promise to after. ```ts const longRunning = () => { // assume this resolves after the response await new Promise((resolve) => setTimeout(resolve, 100)) // response is done after(() => { await headers() // ❌ should be allowed, but throws }) } after(longRunning()) // or waitUntil, doesn't matter ``` The logic we used in the check was incorrect and assumed that an `afterTaskStore` was present, but we can only wrap callbacks in this ALS - we cannot change the async context of already-running promises. One noteworthy change is that i had to exit `actionAsyncStorage` when we do a render after the server action. Otherwise the checks in `isRequestApiAllowedInCurrentPhase` might believe that they're still in a server action when they aren't.github.com-vercel-next.js · 7fc1a200 · 2026-06-19
- 0.7ETVfix: after(callback) called after response end (#94974) Fixes a bug where if `after(callback)`was called after the response ended, and no `after(callback)` calls occurred earlier, the callback would never run. Implementation-wise, what happens is: 1. when the first callback is scheduled, we do `runCallbacksOnClose()`, which waits until `onClose` fires and starts the callback queue 2. but if no `after(callback)` calls occurred before the response ended, then by the time we got to `runCallbacksOnClose()`, `onClose` has already fired, so we'd wait forever We now start listening for `onClose` when the context is initialized and track the state (`isRequestClosed`), which avoids this. I also did another drive-by fix here: if we only had `after(promise)` calls, we wouldn't switch the workUnitStore's phase to `'after'`, because we were only doing that in the `after(callback)` codepath. This is relevant in #94964 which fixes some bugs around `after(promise)`.github.com-vercel-next.js · d538cfb7 · 2026-06-19
- 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
- 0.6ETV[PP] Instant validation - error for unguarded static params (#94595) Follow-up to #95151, implementing support for validating awaits of static params (i.e. when using `generateStaticParams`) An app shell cannot contain any link data. This poses a challenge when we're trying to re-use the dev render for instant validation, because we have to make a choice: 1. either we resolve static params (from `generateStaticParams`) in the `Static` stage, and get an accurate *static HTML shell* (used for the initial navigation), but an incorrect app shell 2. or we resolve them in the `ShellRuntime` stage and we get an accurate App Shell, but cannot validate a static HTML shell (which would contain static params) We'll use order 2 in the main render whenever client-navigating to a page with `partialPrefetching` enabled. We still need a render with order 1 for Static Shell Validation, so we perform a second, partial render that aborts before the dynamic stage (because only instant navigation needs the dynamic stage). As an optimization, we can skip the secondary render if the page doesn't have static params, because in that case the two orders are equivalent. If we're performing an initial load, we'll use order 1 (to reflect the HTML shell). in this case, we'll do a full secondary render with order 2. Same as above, we can skip the secondary render if there's no static params. --- Where params resolve (order 1 vs order 2) is controlled by `requestStore.needsSessionShell`. We then end up with two sets of "validation inputs" (mainly rendered chunks) that we can feed into static and instant validation respectively. Also note that this PR doesn't touch `environmentName`, because that required changing too many tests. This will be addressed in a follow-up.github.com-vercel-next.js · 576d3a33 · 2026-07-01