Commit Graph

14 Commits

Author SHA1 Message Date
Andrew Clark 12b802b259 Convert per-segment prefetches to NavigationFlightResponse format (#96877)
This is the last of the changes to convert all RSC requests to a single
unified response type, NavigationFlightResponse.

The old format was a positional array whose ordering had to be kept in
sync by hand between the server and the client. The new format is the
same root-anchored tree used by every other response, so the dedicated
client write path for per-segment responses is deleted and these
responses go through the shared one.

A few functions are renamed as their roles change:
convertServerPatchToFullTree becomes createNavigationSeed (it now also
accepts trees with no base to overlay), fetchSegmentsOnCacheMiss becomes
fetchSegmentPrefetchesUsingStaticRequest (the counterpart of
fetchSegmentPrefetchesUsingRuntimeRequest), and writeSeedDataIntoCache
becomes writeTreeDataIntoCache (seed data no longer exists as a
concept).

During the unification, a few inconsistencies and oversights were caught
and fixed: response data with no matching pending entry is written as a
detached entry instead of dropped, and a response's byte size is spread
across the entries it actually fulfilled rather than the old chain-based
count. The fallback retry loop also marks itself pending before the
first cache write instead of after, closing a window where the scheduler
could spawn a duplicate revalidation.

The second commit unifies cache keying for prefetched segments. There
were two implementations of the logic that decides which cache key a
prefetched segment is written to, one for per-segment responses and one
for responses produced by a live render. Now there's one: if the server
reported which params the segment depends on, key the entry using those;
otherwise fall back to the keying implied by how the payload was
fetched. A segment is only treated as param-independent when the server
says so. Two precedence fixes were needed to merge them, and a few other
inconsistencies found during the unification are fixed along the way;
see the commit message for details.
2026-08-13 18:18:23 -04:00
Andrew Clark 0c5881a374 Unify how server responses are written into the client cache (#96876)
Every flow that writes a server response into the client cache now goes
through a single function, writeServerResponseIntoCache.

This is preparation for subsequent steps in this stack that migrate even
more callers to this unified path.

While we're modifying it, fetchSegmentPrefetchesUsingDynamicRequest is
renamed to fetchSegmentPrefetchesUsingRuntimeRequest: these responses
are produced by a runtime request, and "dynamic" refers to non-cached
data, which prefetch responses never contain.

One subtle fix that fell out of the unification: a runtime prefetch
response that carries no transport data used to make the flow bail
before writing anything; now the shared function rejects the pending
entries it was given, the same as a build id mismatch.
2026-08-13 18:18:23 -04:00
Marcos Hernanz aeddff1556 Fix spelling in two comments (#97136)
### What?

Fix two spelling mistakes in comments.

### Why?

Readability.

### How?

Updated comment text only.

Attribution: This change was originally authored by @adityagiri3600 in
#90594.

Co-authored-by: Aditya Giri <74224708+adityagiri3600@users.noreply.github.com>
2026-08-10 16:32:30 -07: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
Andrew Clark bdbbb1e887 Block prefetch task until sufficient response is received (#96017)
This is a refactor of the prefetching algorithm to block the prefetch
task from completing until the response is received.

Currently, a prefetch task is fire-and-forget: once a prefetch's
requests are spawned, the scheduler marks the originating task as
complete regardless of what the server ends up returning. However,
certain planned optimizations require us to verify the response from the
server to determine whether additional work is necessary. For example,
we may attempt to do a static prefetch instead of a runtime prefetch if
we optimistically assume that the static prefetch does not vary on
runtime data. We need to verify that assumption is correct before
marking the task as complete.

Now, whenever a pass over the task spawns a segment request, or
encounters an entry whose response hasn't arrived yet, the task
registers itself on the entry and exits as Blocked instead of Done. When
the entry resolves, the blocked task is pinged and the pass re-runs
against the received data. Only a pass that observes every response it
cares about advances the task to the next phase or completes it.

Rejected entries are handled per segment: a rejection counts as an
observed response, so the pass skips that segment and keeps prefetching
the rest. The task never registers on a rejected entry, since nothing
ever pings one. Retries are governed by the existing per-entry logic.

Also fixes a cache bug this exposed: when a revalidation response is
keyed at a more generic vary path than the stale partial entry that
prompted it (because the segment turned out not to vary on some param),
the stale entry shadowed the result on every lookup — the upgraded data
was unreachable and the revalidation was wasted. With blocking, that
dead end became an infinite loop: each fulfillment re-pinged the blocked
task, which re-read the same stale entry and spawned another
revalidation. Upserting (or re-keying) a segment now deletes settled
entries at more specific vary paths that the incoming entry supersedes.

The router-act test helper's "at least one request initiated" watchdog
is adjusted to account for the new blocking behavior: countable requests
can now be legitimately gated on an in-flight App Shell response (which
the watchdog intentionally doesn't count). On expiry it now proceeds to
response processing if the act scope intercepted a shell request, and
keeps waiting while shell requests are in flight elsewhere on the page,
instead of timing out.
2026-07-27 19:03:16 -04:00
Andrew Clark f76af5ca6e Make router-act ignore App Shell prefetch requests (#94580)
When App Shells is enabled, prefetching is split into two phases: a
prefetch for the shell, and a separate phase for the per-page content.

This would cause a handful of the existing prefetch-related tests to
fail once App Shells is turned on. However, the fact that the shell is
prefetched separately from the page content is an incidental detail of
how prefetching works. The App Shell is conceptually part of the route,
not part of the "prefetch". Or in other words, App Shell prefetching has
more in common with incremental module loading than it does with
prefetching in the traditional sense used in the context of web apps.

So, this updates router-act to ignore App Shell requests for the
purposes of making assertions about prefetch responses. They're still
captured, fulfilled, and awaited — they just don't participate in the
matching logic. Tests that specifically assert on App Shell behavior can
opt back in with `createRouterAct(page, { includeAppShellRequests: true
})`.

This is groundwork for the next PR, which enables App Shells by default.
With these requests ignored, most prefetch tests pass unchanged
regardless of whether App Shells is on.

<!-- NEXT_JS_LLM_PR -->
2026-06-09 10:37:59 -07:00
Andrew Clark 138092696c [Segment Bundling] [Scaffolding] Ensure inlining hint correctness (#91320)
Inlining hints (which segments to bundle together) are computed once at
build time by measuring gzip sizes, then persisted to
`prefetch-hints.json`. There are several scenarios where these hints may
not be available at render time. This commit addresses each scenario to
ensure the client always receives correct hints and never enters a bad
state.

### Build-time static pages (stale hints)

The initial RSC payload baked into the HTML is generated before hints
are computed, since hint computation requires at least one completed
render to measure sizes. The server marks these trees with
`InliningHintsStale`. The client immediately expires the route cache
entry so that the next prefetch re-fetches the correct tree from the
`/_tree` response. The segment data doesn't need re-fetching since it
was already cached from the initial HTML payload.

### ISR / revalidation

Hints are always available from the manifest. A missing entry is an
internal error — it means the build pipeline failed to produce hints for
a route that needs them.

### Fully dynamic routes at runtime

No hints exist and none will ever be computed. Every segment gets
`PrefetchDisabled`, which tells the client not to attempt prefetching.
This avoids an infinite re-fetch loop that would occur if the client
kept trying to fetch "correct" hints.

### Unified response format

Also simplifies the segment prefetch response format: every response is
now a `SegmentPrefetchResponse` with a top-level `buildId` and a `data`
array, treating a single-segment response as a bundle that happens to
have length one. This unifies the format so that bundled multi-segment
responses (added in a follow-up) require no additional client parsing
logic.
2026-03-22 22:42:48 -04:00
Andrew Clark 299e1ff318 Remove revalidation settling period from router-act (#90531)
The 500ms settling period in router-act's `act()` polled for 500ms after
the request queue emptied before exiting. This was added to accommodate
the 300ms revalidation cooldown in the client router — after a server
action calls `revalidatePath`/`revalidateTag`, the client delays
re-prefetching for 300ms. The settling period ensured `act()` waited
long enough to catch the resulting re-prefetch.

The settling period applied to every `act()` call, not just ones
involving revalidation, adding ~500ms per call across 28 test files.
Remove it entirely and instead use Playwright's fake clock
(`page.clock.install` / `page.clock.fastForward`) in the revalidation
tests to deterministically control the cooldown. This is both faster and
architecturally cleaner — the test utility no longer embeds knowledge of
a specific client-side timer.
2026-02-25 18:36:04 -05:00
Hendrik Liebau 074ba4a953 [test] Improve fetch timeout error stack for act (#90261)
Before:

```
    apiRequestContext.fetch: Request timed out after 30000ms
    Call log:
      - → GET http://localhost:55200/refetch-on-new-base-tree/a?_rsc=1u5ob
      -   <snip>

      225 |             // server; we pass the request to the server the immediately.
      226 |             result: (async () => {
    > 227 |               const originalResponse = await page.request.fetch(request, {
          |                                                           ^
      228 |                 maxRedirects: 0,
      229 |               })
      230 |

      at fetch (lib/router-act.ts:227:59)
      at lib/router-act.ts:245:13
      at routeHandler (lib/router-act.ts:257:7)
```

After:

```
    apiRequestContext.fetch: Request timed out after 30000ms
    Call log:
      - → GET http://localhost:61662/refetch-on-new-base-tree/a?_rsc=1u5ob
      -   <snip>

      264 |
      265 |     // Reveal the links to trigger prefetches
    > 266 |     await act(async () => {
          |           ^
      267 |       await linkALinkVisibilityToggle.click()
      268 |       await linkBLinkVisibilityToggle.click()
      269 |     }, [

      at Object.act (e2e/app-dir/segment-cache/revalidation/segment-cache-revalidation.test.ts:266:11)
```

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->
2026-02-20 13:02:16 -08:00
Andrew Clark 3ff99bd0e8 Re-implement hmr-refresh in terms of refresh (#87135)
Based on:
- #87134 

---

hmr-refresh-reducer is identical to refresh-reducer except for the fact
that it doesn't evict the prefetch cache. So this refactors both
implementations to call the same shared function.
2025-12-15 18:27:14 -05:00
Andrew Clark bfa5387236 Skip request if "full" prefetch is already pending (#86405)
If a navigation happens while a prefetch for a segment is still pending,
we intentionally don't wait for the prefetch to complete before sending
a navigation request, because we don't know yet whether the prefetch
response will be complete or partial. So, we immediately send a new
navigation request for the data, even at the risk of some duplication,
in order to avoid a waterfall.

However, there is one case where it's safe to assume the prefetch
response will be complete — when the prefetch is initiated via `<Link
prefetch={true}>` (or, equivalently, the "full" option supported by
router.prefetch()). In this case, the client explicitly instructs the
server not to omit any dynamic or runtime data. Therefore, it's OK to
skip over it during the navigation request.
2025-11-22 17:13:06 +00:00
Zack Tanner c56789599e [segment cache]: delay revalidation prefetch pings 300ms (#84981)
After performing a router revalidation (ie in a server action, or
calling router.refresh()), we currently immediately re-prefetch any
links on the page to ensure that subsequent navigations will be able to
see any changed data. This generally works fine when self-hosting/via
`next start` because the same server that is responsible for writing to
the cache also handles serving it. However, when Next.js deployed and
fronted by an external cache handler/CDN (eg deployed to Vercel), it can
take some time to propagate. On Vercel, the documented max amount of
time is 300ms. To alleviate stale data that could come about as a result
of this, we will wait 300ms before triggering the prefetch again.
2025-10-17 13:02:27 -07:00
Wyatt Johnson 43c6ccfdd5 Fix parallel routes ignoring generateStaticParams from primary route (#84889)
### What?

Fixes a bug where `generateStaticParams` from the primary route was
being ignored when dynamic segments existed in both regular routes
(`children`) and parallel routes (e.g., `@breadcrumbs`), causing routes
that should have been statically generated to return 404 errors.

Also fixes a related redirect loop issue that occurred when serving 404
pages with RSC request header validation enabled.

### Why?

**Primary Issue: Segment Deduplication**

The `collectAppPageSegments` function had two code paths for
deduplicating segments when the same dynamic parameter (like `[slug]`)
appeared in both the primary route tree and parallel routes:

1. **Individual segment processing** - Correctly preferred non-parallel
segments
2. **Batch processing at page boundaries** - Blindly overwrote existing
segments without preference checking

Due to BFS traversal order, parallel route segments would overwrite the
correct `children` route segments during batch processing. This marked
parameters as `isParallelRouteSegment: true`, which:
- Prevented the build system from recognizing them as regular route
parameters
- Caused `generateStaticParams` to be ignored during prerendering
- Resulted in all routes being treated as dynamic-only (404s with
`dynamicParams = false`)

This violated the architectural invariant that parallel routes are
"secondary" to the main route hierarchy - the primary route tree should
determine URL structure and static generation behavior.

**Secondary Issue: 404 Page Redirect Loop**

When serving a 404 page (NoFallbackError), the experimental RSC request
header validation would cause an infinite redirect loop. This happened
because:
- Headers are already stripped when serving the 404 page
- The validation comparison would always fail
- This triggered a redirect, which would hit the same 404, creating a
loop

### How?

**Segment Preference Fix (`app-segments.ts`):**

Applied consistent segment preference logic to both deduplication code
paths in `collectAppPageSegments` and `collectFallbackRouteParams`,
ensuring segments from the `children` route are always preferred over
parallel route segments. This preserves the following guarantees:

1. **URL structure is determined by the primary route tree** - Parallel
routes derive their parameters from `children`, not vice versa
2. **`generateStaticParams` works correctly** - Static generation is
controlled by the primary route's configuration
3. **Parallel routes remain decorative** - They enhance the page without
affecting core routing behavior

**Redirect Loop Fix (`base-server.ts`):**

Skip RSC request header validation when serving 404 pages (`!is404Page`
condition), preventing the redirect loop caused by already-stripped
headers failing the validation check.

**Test Coverage:**

Added comprehensive e2e test suite covering:
- Root parameters with `generateStaticParams` and `dynamicParams =
false`
- Dynamic child routes with and without `generateStaticParams`
- Parallel route slots with shared dynamic segments
- Prefetch behavior to ensure no redirect loops during preloading

Fixes #

NAR-446
2025-10-15 17:02:57 -06:00
Zack Tanner dd5e3ad611 [ci]: move router-act to top level testing utils (#84653)
We're starting to use this pattern in more places, so this moves `router-act` into our top level testing utils.
2025-10-08 17:01:40 -07:00