use fetch priority for app prefetches (#67356)
This leverages [fetch priority](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#priority) to ensure automatic prefetching as a result of visiting a page is sent with "low" priority, to signal to the browser it can prioritize more important work necessary for rendering the page. A "temporary" prefetch (ie one that is created when there wasn't an existing prefetch cache entry on navigation) will use a "high" priority because it's critical to the navigation event. All other cases will be "auto" which is the current default.
This commit is contained in:
parent
73b552a9a5
commit
aa832c3b2d
2 changed files with 105 additions and 3 deletions
|
@ -56,6 +56,8 @@ export async function fetchServerResponse(
|
|||
[NEXT_ROUTER_STATE_TREE]: string
|
||||
[NEXT_URL]?: string
|
||||
[NEXT_ROUTER_PREFETCH_HEADER]?: '1'
|
||||
// A header that is only added in test mode to assert on fetch priority
|
||||
'Next-Test-Fetch-Priority'?: RequestInit['priority']
|
||||
} = {
|
||||
// Enable flight response
|
||||
[RSC_HEADER]: '1',
|
||||
|
@ -99,13 +101,28 @@ export async function fetchServerResponse(
|
|||
}
|
||||
}
|
||||
|
||||
// Add unique cache query to avoid caching conflicts on CDN which don't respect to Vary header
|
||||
// Add unique cache query to avoid caching conflicts on CDN which don't respect the Vary header
|
||||
fetchUrl.searchParams.set(NEXT_RSC_UNION_QUERY, uniqueCacheQuery)
|
||||
|
||||
// When creating a "temporary" prefetch (the "on-demand" prefetch that gets created on navigation, if one doesn't exist)
|
||||
// we send the request with a "high" priority as it's in response to a user interaction that could be blocking a transition.
|
||||
// Otherwise, all other prefetches are sent with a "low" priority.
|
||||
// We use "auto" for in all other cases to match the existing default, as this function is shared outside of prefetching.
|
||||
const fetchPriority = prefetchKind
|
||||
? prefetchKind === PrefetchKind.TEMPORARY
|
||||
? 'high'
|
||||
: 'low'
|
||||
: 'auto'
|
||||
|
||||
if (process.env.__NEXT_TEST_MODE) {
|
||||
headers['Next-Test-Fetch-Priority'] = fetchPriority
|
||||
}
|
||||
|
||||
const res = await fetch(fetchUrl, {
|
||||
// Backwards compat for older browsers. `same-origin` is the default in modern browsers.
|
||||
credentials: 'same-origin',
|
||||
headers,
|
||||
priority: fetchPriority,
|
||||
})
|
||||
|
||||
const responseUrl = urlToUrlWithoutFlightMarker(res.url)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { nextTestSetup } from 'e2e-utils'
|
||||
import { check, waitFor } from 'next-test-utils'
|
||||
|
||||
import { check, waitFor, retry } from 'next-test-utils'
|
||||
import type { Page, Request } from 'playwright'
|
||||
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'
|
||||
|
||||
const browserConfigWithFixedTime = {
|
||||
|
@ -347,4 +347,89 @@ describe('app dir - prefetching', () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetch priority', () => {
|
||||
it('should prefetch links in viewport with low priority', async () => {
|
||||
const requests: { priority: string; url: string }[] = []
|
||||
const browser = await next.browser('/', {
|
||||
beforePageLoad(page: Page) {
|
||||
page.on('request', async (req: Request) => {
|
||||
const url = new URL(req.url())
|
||||
const headers = await req.allHeaders()
|
||||
if (headers['rsc']) {
|
||||
requests.push({
|
||||
priority: headers['next-test-fetch-priority'],
|
||||
url: url.pathname,
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
await browser.waitForIdleNetwork()
|
||||
|
||||
await retry(async () => {
|
||||
expect(requests.length).toBeGreaterThan(0)
|
||||
expect(requests.every((req) => req.priority === 'low')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should prefetch with high priority when navigating to a page without a prefetch entry', async () => {
|
||||
const requests: { priority: string; url: string }[] = []
|
||||
const browser = await next.browser('/prefetch-false/initial', {
|
||||
beforePageLoad(page: Page) {
|
||||
page.on('request', async (req: Request) => {
|
||||
const url = new URL(req.url())
|
||||
const headers = await req.allHeaders()
|
||||
if (headers['rsc']) {
|
||||
requests.push({
|
||||
priority: headers['next-test-fetch-priority'],
|
||||
url: url.pathname,
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
await browser.waitForIdleNetwork()
|
||||
|
||||
expect(requests.length).toBe(0)
|
||||
|
||||
await browser.elementByCss('#to-prefetch-false-result').click()
|
||||
await retry(async () => {
|
||||
expect(requests.length).toBe(1)
|
||||
expect(requests[0].priority).toBe('high')
|
||||
})
|
||||
})
|
||||
|
||||
it('should have an auto priority for all other fetch operations', async () => {
|
||||
const requests: { priority: string; url: string }[] = []
|
||||
const browser = await next.browser('/', {
|
||||
beforePageLoad(page: Page) {
|
||||
page.on('request', async (req: Request) => {
|
||||
const url = new URL(req.url())
|
||||
const headers = await req.allHeaders()
|
||||
if (headers['rsc']) {
|
||||
requests.push({
|
||||
priority: headers['next-test-fetch-priority'],
|
||||
url: url.pathname,
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
await browser.elementByCss('#to-dashboard').click()
|
||||
await browser.waitForIdleNetwork()
|
||||
|
||||
await retry(async () => {
|
||||
const dashboardRequests = requests.filter(
|
||||
(req) => req.url === '/dashboard'
|
||||
)
|
||||
expect(dashboardRequests.length).toBe(2)
|
||||
expect(dashboardRequests[0].priority).toBe('low') // the first request is the prefetch
|
||||
expect(dashboardRequests[1].priority).toBe('auto') // the second request is the lazy fetch to fill in missing data
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue