instant(): Only render shell, unless prefetch prop is set#95150
Merged
Conversation
Contributor
Tests PassedCommit: 6ebbf37 |
Contributor
Stats cancelledCommit: 6ebbf37 |
samselikoff
approved these changes
Jun 25, 2026
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.
89bce21 to
6ebbf37
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.