Files
Andrew ClarkandGitHub 203177bb75 Gate partialFallback behavior behind partialPrefetching flag (#96074)
`partialFallback` is an internal flag in the build output that controls
the behavior of ISR. When it's enabled, in a Cache Components app, pages
that are not generated at build time are upgraded to a full ISR entry
upon the first request.

This is a useful feature but there's a risk it could lead to an
explosion in ISR costs, because even a prefetch request is sufficient to
trigger an upgrade.

The desired behavior is that we only trigger an upgrade for an actual
navigation request, or if a link opts into prefetching with the
`prefetch` prop.

Currently there's no way for ISR to differentiate between a shell
prefetch request (no prefetch prop on the link) and an actual prefetch
(`prefetch={true}`). The plan is to introduce additional configuration
to the build output that specifies which headers should trigger which
operations.

So by default, this PR turns the `partialFallback` flag back off until
the new mechanism is available to us.

However, for apps that opt into Partial Prefetching, we will continue to
set `partialFallback` to true. That's because in a Partial Prefetching
app, per-link prefetch requests are always opt-in. So even without
additional support at the ISR behavior, we avoid an unexpected surge in
ISR costs by not sending the prefetching request in the first place.
There will still be a single request per distinct route (_not_ per
distinct URL!), which we will optimize later.
2026-07-22 17:06:41 -04:00

42 lines
1.6 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import type { NextAdapter } from 'next'
describe('adapter-partial-fallback-disabled', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should not emit partial fallback metadata when partialPrefetching is disabled', async () => {
const { outputs }: Parameters<NextAdapter['onBuildComplete']>[0] =
await next.readJSON('build-complete.json')
const withGspPrerender = outputs.prerenders.find(
(output) => output.pathname === '/with-gsp/[slug]'
)
const withGspRouteTreePrerender = outputs.prerenders.find(
(output) =>
output.pathname === '/with-gsp/[slug].segments/_tree.segment.rsc'
)
const withGspOtherSegmentPrerenders = outputs.prerenders.filter(
(output) =>
output.pathname.startsWith('/with-gsp/[slug].segments/') &&
output.pathname !== '/with-gsp/[slug].segments/_tree.segment.rsc'
)
expect(withGspPrerender).toBeDefined()
expect(withGspRouteTreePrerender).toBeDefined()
expect(withGspOtherSegmentPrerenders.length).toBeGreaterThan(0)
// Without `partialPrefetching` enabled, the route that would otherwise
// qualify for a partial fallback shell must not emit the partialFallback
// config, and its fallback shell collapses to the shared shell (empty
// allowQuery).
expect(withGspPrerender.config.partialFallback).toBeUndefined()
expect(withGspPrerender.config.allowQuery).toEqual([])
expect(withGspRouteTreePrerender.config.partialFallback).toBeUndefined()
for (const output of withGspOtherSegmentPrerenders) {
expect(output.config.partialFallback).toBeUndefined()
}
})
})