Skip to content

instant(): Only render shell, unless prefetch prop is set#95150

Merged
acdlite merged 1 commit into
canaryfrom
worktree-navlock-shell-restriction
Jun 25, 2026
Merged

instant(): Only render shell, unless prefetch prop is set#95150
acdlite merged 1 commit into
canaryfrom
worktree-navlock-shell-restriction

Conversation

@acdlite

@acdlite acdlite commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

When using the instant() API, or the Navigation Inspector, the prefetched state should match what a user is most likely to see in production with a warm cache. So, when Partial Prefetching is enabled, and the Link does not have a prefetch prop, only the shell should be allowed to render.

Before this PR, the entire prefetched state would display, regardless of the prefetch configuration of the route and the link.

This is essential to prevent false negatives in tests that assert on the prefetched state: the test must not depend on data that wouldn't be prefetched in a real production environment.

As part of the fix, I also updated the logic to ignore any cache entries that were already present before the instant() lock was acquired, to ensure the test is not "polluted" by earlier prefetches or navigations. Theoretically we could optimize this in the future but this is the cleanest way to ensure this property for now.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: 6ebbf37

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Stats cancelled

Commit: 6ebbf37
View workflow run

@acdlite acdlite marked this pull request as ready for review June 25, 2026 02:59
When using the instant() API, or the Navigation Inspector, the prefetched state
should match what a user is most likely to see in production with a warm cache.
So, when Partial Prefetching is enabled, and the Link does not have a prefetch
prop, only the shell should be allowed to render.

Before this PR, the _entire_ prefetched state would display, regardless of the
prefetch configuration of the route and the link.

This is essential to prevent false negatives in tests that assert on the
prefetched state: the test must not depend on data that wouldn't be prefetched
in a real production environment.

As part of the fix, I also updated the logic to ignore any cache entries that
were already present before the `instant()` lock was acquired, to ensure the
test is not "polluted" by earlier prefetches or navigations. Theoretically we
could optimize this in the future but this is the cleanest way to ensure this
property for now.
@acdlite acdlite force-pushed the worktree-navlock-shell-restriction branch from 89bce21 to 6ebbf37 Compare June 25, 2026 03:23
@acdlite acdlite enabled auto-merge (squash) June 25, 2026 03:23
@acdlite acdlite merged commit dcf649f into canary Jun 25, 2026
227 of 230 checks passed
@acdlite acdlite deleted the worktree-navlock-shell-restriction branch June 25, 2026 03:54
unstubbable added a commit that referenced this pull request Jun 25, 2026
…95067)

The Instant Navigation lock makes a development render reflect the
route's prefetched state: while the lock is held, a navigation shows
only what was prefetched and keeps navigation-time data deferred. Both
the `instant()` testing API from `@next/playwright` and the Instant
Navigation devtools rely on it. The client side of that behavior,
restricting the navigation read to the prefetched shell unless the link
opts into a runtime prefetch and ignoring cache entries acquired before
the lock, landed separately in #95150, which this change builds on. What
remained were two ways the development app render diverged from what a
production build serves, both in `app-page.ts`, so this PR contains no
client-side router changes.

While the lock is held, the app render now permits an empty static
shell. A route that reads dynamic data such as `cookies()` outside any
`<Suspense>` boundary has no static shell, so the on-demand render
previously threw a static generation bailout, served an error page, and
entered a `/_tree` redirect loop that committed the blocked data.
Permitting an empty shell lets the render emit the shell instead. The
override is scoped to the prefetch render made while the lock is held, a
document request or a `'1'` static prefetch; a regular dynamic
navigation, including the one that commits once the lock releases, runs
without it. So the validation that flags a blocking route is not
weakened: production validation runs at build time, and the development
validation still runs and is shown in the dev overlay.

The development fallback-shell render now reads the per-URL
`fallbackParams` request meta that base-server derives for the requested
URL, so the instant shell defers exactly the params a production
prefetch would: `generateStaticParams`-covered params resolve in the
shell and only the uncovered ones are deferred. When the URL is fully
covered the meta is absent and nothing is deferred. That per-URL
computation landed in #95066, which this change depends on.

This suite now runs with App Shells enabled, which is the default under
Cache Components. #94516 had temporarily forced it back to `false` here
while the prefetch behavior settled and left the migration as a
follow-up; re-enabling it is what lets these tests exercise the
app-shell prefetch path the changes above target.

New end-to-end coverage exercises the cases this render path affects. A
deeper-segment blocking navigation must stay parked on the committed
parent while the lock is held. Two mixed routes pair a
`generateStaticParams`-covered param with an uncovered one: a plain
route where a normal navigation surfaces only the covered param while
the uncovered param and a request-time `connection()` sibling stay
deferred, and an `allow-runtime` route where a `prefetch={true}` link
additionally surfaces the uncovered param from the runtime prefetch. A
blocking route that reads request data outside any `<Suspense>` boundary
cannot be built for production, so it lives in a development-only
fixture, guarded so the production job registers only a placeholder. The
default fixture moves the dev-tools indicator to `bottom-right` so the
Instant Navigation panel does not overlap the left-aligned test links
during a navigation.

One client-navigation cookie case is marked `it.failing`: on a
non-partial route the speculative static prefetch is fuller than the
app-shell render and supersedes it in the segment cache, so the cookie
that only the app shell carries never reaches the instant shell.
#95150's shell handling only engages under partial prefetching. Closing
this needs a separate server-side change so that a route reading
`cookies()` during app-shell generation opts into either partial
prefetching, where only the app shell is fetched and nothing fuller can
supersede it, or a runtime prefetch, where the speculative prefetch
carries the cookie and no longer regresses what the app shell initially
showed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants