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_ROUTER_STATE_TREE]: string
|
||||||
[NEXT_URL]?: string
|
[NEXT_URL]?: string
|
||||||
[NEXT_ROUTER_PREFETCH_HEADER]?: '1'
|
[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
|
// Enable flight response
|
||||||
[RSC_HEADER]: '1',
|
[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)
|
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, {
|
const res = await fetch(fetchUrl, {
|
||||||
// Backwards compat for older browsers. `same-origin` is the default in modern browsers.
|
// Backwards compat for older browsers. `same-origin` is the default in modern browsers.
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers,
|
headers,
|
||||||
|
priority: fetchPriority,
|
||||||
})
|
})
|
||||||
|
|
||||||
const responseUrl = urlToUrlWithoutFlightMarker(res.url)
|
const responseUrl = urlToUrlWithoutFlightMarker(res.url)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { nextTestSetup } from 'e2e-utils'
|
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'
|
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'
|
||||||
|
|
||||||
const browserConfigWithFixedTime = {
|
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