Files
Andrew ClarkandGitHub 72a80d7ff3 Read from segment cache unknown routes (#87293)
Based on:

- https://github.com/vercel/next.js/pull/87203
- https://github.com/vercel/next.js/pull/87256

---

Currently, there's a cliff when navigating to a route that is unknown to
the client. The prefetch cache is bypassed completely because we don't
know the structure of the target route, and therefore we don't know
which segments we might be able to use to construct a loading state (or
even a fully cached response).

However, because RSC responses are streaming, we can take advantage of
the fact that the first chunk of the response contains the route tree.
So as soon as we receive the first chunk, we can show a cached loading
state to the user (if available), even while we wait for the rest of the
response to arrive from the server.

To access a segment's cache, you need its corresponding RouteTree.
Previously this was not available during an unprefetched navigation
because those objects were only created during a prefetch. But now that
we've migrated the navigation implementation to use RouteTrees, too, we
can access the cache directly inside the main navigation function,
rather than having to do two separate passes like before.

In other words, the only difference between a navigation to an unknown
route versus a prefetched route is the extra latency required to get
those first bytes from the server. Once the route tree resolves, the
rest of the implementation and behavior is identical.
2026-01-11 21:30:25 -05:00

81 lines
2.8 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import type * as Playwright from 'playwright'
import { createRouterAct } from 'router-act'
describe('mismatching prefetch', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
if (isNextDev) {
test('disabled in development', () => {})
return
}
function relativeHref(href: string) {
const url = new URL(href)
return url.pathname + url.search + url.hash
}
it(
'recovers when a navigation rewrites to a different route than the one ' +
'that was prefetched',
async () => {
let page: Playwright.Page
const browser = await next.browser('/mismatching-prefetch', {
beforePageLoad(p: Playwright.Page) {
page = p
},
})
const act = createRouterAct(page)
// Reveal the link to trigger a prefetch of page A.
const toggle = await browser.elementByCss(
'input[data-link-accordion="/mismatching-prefetch/dynamic-page/a?mismatch-rewrite=./b"]'
)
await act(async () => await toggle.click(), {
includes: 'Loading a...',
})
// When we click the link to navigate, the navigation will rewrite to
// a different route than the one that was prefetched.
await act(
async () => {
const link = await browser.elementByCss(
'a[href="/mismatching-prefetch/dynamic-page/a?mismatch-rewrite=./b"]'
)
await link.click()
// Immeidately after the click, the app navigates to the loading state
// that was prefetched, which is for page A.
const pageALoading = await browser.elementById(
'dynamic-page-loading-a'
)
expect(relativeHref(await browser.url())).toBe(
'/mismatching-prefetch/dynamic-page/a?mismatch-rewrite=./b'
)
expect(await pageALoading.text()).toBe('Loading a...')
// Simultaneously, the dynamic content for page A is requested.
},
// When the dynamic request is received, Next.js will discover that the
// route has changed and rewrite to page B. The client will detect a
// mismatch and render again using the new route from the server.
//
// This shouldn't require a second dynamic request, though, because we
// can reuse the data sent by the server. So, page B should only render
// once on the server.
[{ includes: 'Dynamic page b' }]
)
// The redirected page loads successfully.
const pageBContent = await browser.elementById('dynamic-page-content-b')
expect(await pageBContent.text()).toBe('Dynamic page b')
// The browser's URL hasn't changed, because this was a rewrite, not
// a redirect.
expect(relativeHref(await browser.url())).toBe(
'/mismatching-prefetch/dynamic-page/a?mismatch-rewrite=./b'
)
}
)
})