mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
Currently the head (i.e. the viewport and the metadata) is stored in the same cache entry as the route tree. This is not ideal because the head may or may not contain application data, and the response can differ depending on the prefetch strategy. We need to be able to re-fetch the head without also invalidating the route tree. This refactors the Segment Cache to treat the head data as a special kind of "segment". It doesn't appear in the route tree, so there's some logic to account for that, but for the most part it reuses all the existing implementation for fetching and caching normal segment data.
253 lines
9.9 KiB
TypeScript
253 lines
9.9 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import type * as Playwright from 'playwright'
|
|
import { createRouterAct } from 'router-act'
|
|
|
|
describe('segment cache prefetch scheduling', () => {
|
|
const { next, isNextDev } = nextTestSetup({
|
|
files: __dirname,
|
|
})
|
|
if (isNextDev) {
|
|
test('prefetching is disabled', () => {})
|
|
return
|
|
}
|
|
|
|
it('increases the priority of a viewport-initiated prefetch on hover', async () => {
|
|
let act: ReturnType<typeof createRouterAct>
|
|
const browser = await next.browser('/cancellation', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const checkbox = await browser.elementByCss('input[type="checkbox"]')
|
|
await act(
|
|
async () => {
|
|
// Reveal the links to start prefetching, but block the responses from
|
|
// reaching the client. This will initiate prefetches for the route
|
|
// trees, but it won't start prefetching any segment data yet until the
|
|
// trees have loaded.
|
|
await act(async () => {
|
|
await checkbox.click()
|
|
}, 'block')
|
|
|
|
// Hover over a link to increase its relative priority.
|
|
const link2 = await browser.elementByCss('a[href="/cancellation/2"]')
|
|
await link2.hover()
|
|
|
|
// Hover over a different link to increase its relative priority.
|
|
const link5 = await browser.elementByCss('a[href="/cancellation/5"]')
|
|
await link5.hover()
|
|
},
|
|
// Assert that the segment data is prefetched in the expected order.
|
|
[
|
|
// The last link we hovered over should be the first to prefetch.
|
|
{ includes: 'Content of page 5' },
|
|
// The second-to-last link we hovered over should come next.
|
|
{ includes: 'Content of page 2' },
|
|
// Then all the other links come after that. (We don't need to assert
|
|
// on every single prefetch response. I picked one of them arbitrarily.)
|
|
{ includes: 'Content of page 4' },
|
|
]
|
|
)
|
|
})
|
|
|
|
// TODO: This test no longer works as-written because the metadata is now
|
|
// fetched separately from the route tree. Need to rewrite to assert on
|
|
// something that appears in the route tree and is relatively stable, like a
|
|
// static route name.
|
|
it.skip('prioritizes prefetching the route trees before the segments', async () => {
|
|
let act: ReturnType<typeof createRouterAct>
|
|
const browser = await next.browser('/cancellation', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const checkbox = await browser.elementByCss('input[type="checkbox"]')
|
|
|
|
await act(async () => {
|
|
// Reveal the links to start prefetching
|
|
await checkbox.click()
|
|
}, [
|
|
// Assert on the order that the prefetches requests are
|
|
// initiated. We don't need to assert on every single prefetch response;
|
|
// this will only check the order of the ones that we've listed.
|
|
//
|
|
// To detect when the route tree is prefetched, we check for a string
|
|
// that is known to be present in the target page's viewport config
|
|
// (which is included in the route tree response). In this test app, the
|
|
// page number is used in the media query of the theme color. E.g. for
|
|
// page 1, the viewport includes:
|
|
//
|
|
// <meta name="theme-color" media="(min-width: 1px)" content="light"/>
|
|
|
|
// First we should prefetch all the route trees:
|
|
{ includes: '(min-width: 7px)' },
|
|
{ includes: '(min-width: 6px)' },
|
|
{ includes: '(min-width: 5px)' },
|
|
{ includes: '(min-width: 4px)' },
|
|
{ includes: '(min-width: 3px)' },
|
|
|
|
// Then we should prefetch the segments:
|
|
{ includes: 'Content of page 7' },
|
|
{ includes: 'Content of page 6' },
|
|
{ includes: 'Content of page 5' },
|
|
{ includes: 'Content of page 4' },
|
|
{ includes: 'Content of page 3' },
|
|
])
|
|
})
|
|
|
|
it('reserve special bandwidth for the most recently hovered link', async () => {
|
|
let act: ReturnType<typeof createRouterAct>
|
|
const browser = await next.browser('/cancellation', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const checkbox = await browser.elementByCss('input[type="checkbox"]')
|
|
await act(async () => {
|
|
// Reveal the links to start prefetching, but block the responses from
|
|
// reaching the client. This will initiate prefetches for the route
|
|
// trees, but it won't start prefetching any segment data yet until the
|
|
// trees have loaded.
|
|
await act(async () => {
|
|
await checkbox.click()
|
|
}, 'block')
|
|
|
|
// At this point, the network queue is already at max capacity, so
|
|
// revealing more links won't initiate any new requests.
|
|
await act(async () => {
|
|
const showMoreLinksButton = await browser.elementById('show-more-links')
|
|
await showMoreLinksButton.click()
|
|
}, 'no-requests')
|
|
|
|
// However, when hovering over a link, the prefetch task is allowed to
|
|
// exceed the default limit. So hovering over a link does initiate a
|
|
// new request.
|
|
await act(async () => {
|
|
// Hover over a link. This will initiate a request for the route tree,
|
|
// but before we're able to fetch the segments, we'll have already
|
|
// hovered over a different link.
|
|
const link2 = await browser.elementByCss('a[href="/cancellation/2"]')
|
|
await link2.hover()
|
|
|
|
// Immediately hover over a different link.
|
|
const link3 = await browser.elementByCss('a[href="/cancellation/3"]')
|
|
await link3.hover()
|
|
}, [
|
|
// The most recently hovered link is allowed to finish loading its
|
|
// segment data.
|
|
{ includes: 'Content of page 3' },
|
|
// The previously hovered link was downgraded to the default priority,
|
|
// so it should still be blocked.
|
|
{ includes: 'Content of page 2', block: 'reject' },
|
|
])
|
|
}, [
|
|
// Assert that everything else proceeds in the expected order. We don't
|
|
// need to assert on every single prefetch response; I picked just a few
|
|
// of them.
|
|
|
|
// The previously hovered link is the next to finish loading, because
|
|
// even though it was downgraded to the default priority, it was still
|
|
// moved ahead of the other default tasks.
|
|
{ includes: 'Content of page 2' },
|
|
// Next are the links that were revealed by the "Show More" button.
|
|
{ includes: 'Content of page 8' },
|
|
// Then the rest.
|
|
{ includes: 'Content of page 5' },
|
|
])
|
|
})
|
|
|
|
it(
|
|
'cancels a viewport-initiated prefetch if the link leaves the viewport ' +
|
|
'before it finishes',
|
|
async () => {
|
|
let act: ReturnType<typeof createRouterAct>
|
|
const browser = await next.browser('/cancellation', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const checkbox = await browser.elementByCss('input[type="checkbox"]')
|
|
|
|
await act(
|
|
async () => {
|
|
// Reveal the links to start prefetching, but block the responses from
|
|
// reaching the client. Because the router limits the number of
|
|
// concurrent prefetches, not all the links will start prefetching —
|
|
// some of them will remain in the queue, waiting for additional
|
|
// network bandwidth. This test demonstrates that those prefetches
|
|
// will be canceled on viewport exit, too.
|
|
await act(async () => {
|
|
await checkbox.click()
|
|
}, 'block')
|
|
|
|
// Before the prefetch finishes, click the checkbox again to hide
|
|
// the link.
|
|
await checkbox.click()
|
|
},
|
|
// When the outer `act` scope finishes, the route tree prefetch will
|
|
// continue. Normally when the router is done prefetching the route
|
|
// tree, it will proceed to prefetching the segments. However, since
|
|
// the link is no longer visible, it should stop prefetching.
|
|
//
|
|
// Assert that no additional network requests are initiated in this
|
|
// outer scope. If this fails, it suggests that the prefetches were not
|
|
// canceled when the links left the viewport.
|
|
'no-requests'
|
|
)
|
|
}
|
|
)
|
|
|
|
it("reschedules a link's prefetch when it re-enters the viewport", async () => {
|
|
let act: ReturnType<typeof createRouterAct>
|
|
const browser = await next.browser('/cancellation', {
|
|
beforePageLoad(p: Playwright.Page) {
|
|
act = createRouterAct(p)
|
|
},
|
|
})
|
|
|
|
const checkbox = await browser.elementByCss('input[type="checkbox"]')
|
|
|
|
await act(
|
|
async () => {
|
|
// Reveal the links to start prefetching, but block the responses from
|
|
// reaching the client. Because the router limits the number of
|
|
// concurrent prefetches, not all the links will start prefetching —
|
|
// some of them will remain in the queue, waiting for additional
|
|
// network bandwidth. This test demonstrates that those prefetches
|
|
// will be canceled on viewport exit, too.
|
|
await act(async () => {
|
|
await checkbox.click()
|
|
}, 'block')
|
|
|
|
// Before the prefetch finishes, click the checkbox again to hide
|
|
// the link.
|
|
await checkbox.click()
|
|
},
|
|
// When the outer `act` scope finishes, the route tree prefetch will
|
|
// continue. Normally when the router is done prefetching the route
|
|
// tree, it will proceed to prefetching the segments. However, since
|
|
// the link is no longer visible, it should stop prefetching.
|
|
//
|
|
// Assert that no additional network requests are initiated in this
|
|
// outer scope. If this fails, it suggests that the prefetches were not
|
|
// canceled when the links left the viewport.
|
|
'no-requests'
|
|
)
|
|
|
|
// Now we'll reveal the links again to verify that the prefetch tasks are
|
|
// rescheduled, after having been canceled.
|
|
await act(
|
|
async () => {
|
|
await checkbox.click()
|
|
},
|
|
// Don't need to assert on all the prefetch responses. I picked an
|
|
// arbitrary one.
|
|
{ includes: 'Content of page 5' }
|
|
)
|
|
})
|
|
})
|