Files
Andrew ClarkandGitHub 3717cd3dfb Gate partial fallback shell upgrades behind partialPrefetching for next start (#96297)
In #96074 I put the `partialFallback` behavior behind the
`partialPrefetching` flag, so that we don't risk an explosion in ISR
costs for apps that haven't opted into Partial Prefetching. But it turns
out I only handled this for deploy mode — i.e. the build output consumed
by the Vercel adapter. The behavior was still unconditionally on for
`next start`.

The reason it was easy to miss is that `next start` and the adapter
express partial fallback shells through completely separate mechanisms.
The adapter emits a `partialFallback` flag into the build output and
lets the platform's ISR layer perform the shell upgrade. `next start`
has no such flag; the server performs the upgrade itself, in the
compiled page runtime. So the gate I added to the adapter output had no
effect on the self-hosted path.

To understand how the `next start` path was configured, I looked at the
PR that removed the original top-level `partialFallbacks` config flag
(#93859). Before that PR, the behavior was gated in the runtime; when
the flag was removed those gates were deleted and the behavior became
always-on.

The important subtlety is that only the *upgrade* should be gated, not
the shell machinery as a whole. On `next start`, the value that decides
whether a shell can be specialized (`remainingPrerenderableParams`) also
drives core Cache Components behavior — build-time partial prerendering
of params and serving build-time sub-shells — which must stay on
regardless of `partialPrefetching`. So this gates only the two things
that make up the "upgrade on first request" cost risk: the background
ISR revalidation that specializes a shell per request, and the
client-facing `isFallbackUpgradeable` signal that tells the client to
retry a prefetch waiting for that upgrade.

Adds coverage with a `partialPrefetching`-disabled fixture asserting
that fallback shells stay shared rather than specializing per request.
2026-07-29 19:56:49 +00:00

42 lines
1.7 KiB
TypeScript

import { LinkAccordion } from '../components/link-accordion'
export default function Page() {
return (
<ul>
<li>
{/* Prerendered at build time via generateStaticParams — the prefetch
receives the concrete version, never a fallback. */}
<LinkAccordion href="/static-posts/1" prefetch={true}>
Static post 1 (concrete)
</LinkAccordion>
</li>
<li>
{/* Has generateStaticParams (for a different value), so this
un-enumerated param is upgradeable: the server serves a fallback
shell on first prefetch, then regenerates the concrete version in
the background. Used by the recovery test. */}
<LinkAccordion href="/posts/recovery" prefetch={true}>
Post recovery (upgradeable fallback)
</LinkAccordion>
</li>
<li>
{/* No generateStaticParams — the fallback shell can never be upgraded,
so the prefetch shell must NOT be flagged as a fallback (no retry). */}
<LinkAccordion href="/no-static-params/1" prefetch={true}>
No static params 1 (non-upgradeable fallback)
</LinkAccordion>
</li>
<li>
{/* Same upgradeable route as /posts/recovery, but with its own
un-enumerated param so its ISR entry is independent of the recovery
test's. Used by the retry-*limit* test, which forces every prefetch
response to keep returning the fallback so the client retries to
its cap and then stops. */}
<LinkAccordion href="/posts/retry-limit" prefetch={true}>
Post retry-limit (upgradeable fallback)
</LinkAccordion>
</li>
</ul>
)
}