Commit Graph

9 Commits

Author SHA1 Message Date
Janka Uryga a777ebcfae test: retry waiting for ISR fallback in prefetch-static-shell (#98884)
- ISR prerenders can take a moment to propagate in `deploy`, longer than
in `start`, so do a retry loop that waits for the fallback to go away
before running tests that need it
- ISR fallbacks are only upgraded when using adapters, skip the tests
that care about those if using legacy deploy bc they'll fail otherwise
2026-09-18 19:31:05 +02:00
Janka Uryga 9908328192 [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.
2026-09-17 18:31:07 +02:00
Janka Uryga e974969ddd [PPF] Model caches with stale < MIN_SHELL_STALE as prefetch-only data (#98339)
Caches that have `stale < MIN_SHELL_STALE` were using
`makeStageHangingPromise` in prerenders, which (in theory) was meant to
track a runtime data access during a static prerender, which in turn
would make the page use runtime shells. This is semantically incorrect,
because both static and runtime shells would exclude the cache, and both
static and runtime prefetches would include it, so it does not actually
signal runtimeness, only prefetchness. The only thing we should be doing
for these caches is excluding the content, same as
`unstable_prefetch()`.

This PR replaces `makeStageHangingPromise` with
`makePrefetchHangingPromise`, which is not tracked as runtime data at
all. We also use it for `unstable_prefetch()` to signal the
correspondence.

Note that in practice, the usage of `makeStageHangingPromise` in
`use-cache-wrapper` was *technically* fine, because its runtime data
tracking was effectively dead code. We only returned it if
`stagedRendering.finalStage < stage`, but for static prerenders `stage =
staticLinkDataStage = PrefetchStatic` and `finalStage = Static`, so the
condition was never actually true, and we never actually tracked it as a
runtime data access. However semantically it should still not be tracked
as runtime data.
(The condition *can* be true in a runtime prerender that only prerenders
the shell. We don't do any tracking in runtime prerenders, so it was
also a no-op).

(this was extracted from #98129, where we separate runtime data access
tracking into session data and URL data. i couldn't figure which one
`makeStageHangingPromise` should be tracked as, but then i realized it
shouldn't be tracked at all)
2026-09-08 16:22:17 +02:00
Janka Uryga d352f1740a [PPF] Only track runtime accesses when the promise is used (#97165)
Improves how we track accesses of runtime data in static prerenders
(which affects whether a page will use static or runtime shells).
Now, Instead of tracking it when the hanging promise is created, we'll
track it when `then/catch/finally` is called on the promise (which we
use as a signal for being used).

The promise tracking required some novel work, because:

- Using a proxy seems to cause `Promise.then` to show up in call stacks
(we've had issues with proxies throwing off React's IO tracking before,
i believe this is a similar problem)
- patching `then/catch/finally` on the promise directly doesn't work,
because `then` doesn't get called for native `await`

The only way of doing this that i've found is a custom `Promise`
subclass that tracks the access in its `then` implementation. This is
what `TrackedPromise` does.

To improve the debuggability, i've added some debug logs enabled by
`NEXT_PRIVATE_DEBUG_RUNTIME_DATA=1` to detect when a page is deopted to
runtime and what expression caused it.

---

This fixes a false deopt caused by `createServerPathnameForMetadata` -
when called on a route with fallback params, it would always track a
fallback params access even if no icons/og images that actually use it
exist. This meant that we'd never statically optimize any routes that
had fallback params. After this PR, we only track a runtime data access
if a metadata route that uses that params actually exists, in which case
the `pathname` promise gets awaited.
(as a future optimization, we should do the equivalent of vary params
tracking on metadata routes to know if the params are used at all. also,
the logic in `pathname.ts` is generally in need of cleanup)
2026-08-26 18:05:44 +02:00
Janka Uryga 3cdb56c251 [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.
2026-08-21 13:50:23 +02:00
Janka Uryga 1be0ab80c4 [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.
2026-08-20 15:27:11 +02:00
Janka Uryga 6fbd61c152 [PPF] Do not mark complete shell requests as partial (#97503)
The router code did not respect the isPartial byte for runtime app
shells for some reason, potentially leading to unnecessary speculative
prefetch requests even if the shell was in fact complete.

The comments say that the byte on those can't be trusted:

https://github.com/vercel/next.js/blob/d00eb6b6c471c9c51ef997f643b049210d9940a5/packages/next/src/client/components/segment-cache/cache.ts#L2694-L2696
which is not true -- the byte is definitely set to partial when a shell
contains holes (due to URL data or link data).
 
Updates some tests that now no longer trigger a speculative prefetch
because the shell is complete.
(note that our tests use `prefetch = 'partial'` a lot, so some of them
were triggering speculative prefetches even when they shouldn't have.
see #97469 for more)
2026-08-19 23:21:56 +02:00
Andrew Clark 3de2d1a213 Unify allow-runtime with Partial Prefetching (#96106)
Removes the "allow-runtime" prefetch config, and turns its behavior on
implicitly wherever Partial Prefetching is enabled.

The original motivation for "allow-runtime" was to give apps more
control over server costs triggered by prefetches. Until a route
explicitly opts in, prefetches would only be served from the CDN, not
from the server. The problem, though, was it was very confusing to know
when to add or remove this configuration. The incentive for many apps
was to add it everywhere, with no clear signal for when to remove it.

Our updated thinking is that Partial Prefetching itself already provides
sufficient protection against runaway prefetching costs: per-link
prefetches only happen on Link components that explicitly opt in with
the prefetch prop.

The optimizations landed earlier in this stack also make allow-runtime
less necessary: on pages where all the content is statically renderable,
prefetches are served from the static cache and no runtime request is
ever issued; only a page that accesses non-static data is prefetched at
runtime.

The upshot of this decision is that runtime versus static becomes an
internal optimization; the same content gets prefetched regardless of
whether or how Next.js is able to optimize it.
2026-07-28 11:51:29 -04:00
Andrew Clark dfa7f4f713 Attempt static prefetch before resorting to runtime (#96095)
If we're reasonably confident that a segment can be prefetched
statically without omitting data that would have been included during a
runtime prefetch (e.g. cookies), the client should attempt prefetch the
segment statically instead of going straight to a runtime request.

The decision for whether to do this is based on the
ShouldAttemptStaticPrefetch added in previous steps.

If it turns out the static response is not sufficient, then it will fall
back to a runtime request.

This makes prefetching cheaper for pages that are fully statically
renderable. We can make the optimization better in the future with more
reliable per-segment computation of the ShouldAttemptStaticPrefetch, but
the current approach should at least work for fully static pages, which
is what's most important.

This optimization applies during both the Shell phase and the
Speculative phase of the prefetching algorithm.
2026-07-28 11:51:29 -04:00