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:
Zack Tanner 2024-07-01 14:47:56 -07:00 committed by GitHub
parent 73b552a9a5
commit aa832c3b2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 105 additions and 3 deletions

View file

@ -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)

View file

@ -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
})
})
})
})