Files
vercel__next.js/test/development/app-dir/react-performance-track/react-performance-track.test.ts
Janka Uryga 46681d90f0 [PPF] Sync IO is only allowed in the dynamic stage (#95384)
This PR makes Sync IO behavior more restrictive when
`partialPrefetching` is on (either globally or for a page).

- Previously, we allowed `await cookies(); Date.now()` in in segments
that won't ever be runtime prefetched. This was achieved by separating
segments into "early" and "late" stages and only erroring in the "early"
ones.
- After this PR, it will always be an error to do sync IO anywhere other
than after `io()`/`connection()`/uncached IO (i.e. outside the Dynamic
stage).

We're doing this mainly because with `partialPrefetching`, `cookies()`
resolves in App Shells, and sync IO causes a severe deopt in any
prerender. `allow-runtime` also opts a route into `partialPrefetching`,
which means that even the segments above the runtime prefetch boundary
will need an App Shell and can't tolerate Sync IO.

This PR removes all early/late stage separation, because we no longer
need to vary the Sync IO behavior on the segment. Instead, we now pick
whether a stageController should use
- `SyncIOMode.AllowedInRuntimeOrDynamic` (legacy)
- `SyncIOMode.AllowedInDynamic` (`partialPrefetching`)
- `SyncIOMode.Untracked` if it needs to opt out

I've removed the existing tests that asserted that sync IO is allowed in
segments above the `allow-runtime` boundary. Instead, we check that sync
io either errors (if partialPrefetching is on) or is allowed (if
partialPrefetching is off)

Closes NAR-855
2026-07-10 15:36:41 +00:00

133 lines
3.9 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
describe('react-performance-track', () => {
// false is the default when visiting pages as an ordinary user.
// true is the default when having Chrome DevTools open.
// Hardcoded for now since most of the actual behavior is not intended.
const disableCache = false
const extraHTTPHeaders = disableCache
? { 'Cache-Control': 'no-cache' }
: undefined
const { next } = nextTestSetup({
files: __dirname,
})
it('should show setTimeout', async () => {
const browser = await next.browser('/set-timeout', { extraHTTPHeaders })
await browser.elementByCss('[data-react-server-requests-done]', {
state: 'attached',
})
const track = await browser.eval('window.reactServerRequests.getSnapshot()')
expect(track).toEqual(
expect.arrayContaining([
{ name: '\u200bsetTimeout', properties: [] },
{ name: '\u200bsetTimeout', properties: [] },
])
)
})
it('should show fetch', async () => {
const browser = await next.browser('/fetch', { extraHTTPHeaders })
await browser.elementByCss('[data-react-server-requests-done]', {
state: 'attached',
})
const track = await browser.eval('window.reactServerRequests.getSnapshot()')
expect(track).toEqual(
expect.arrayContaining([
{
// React might decide to display the shorthand in round brackets differently.
// Double check with React changes if a shorthand change is intended.
name: '\u200bfetch (…/random)',
properties: expect.arrayContaining([
['status', '200'],
['url', '"https://next-data-api-endpoint.vercel.app/api/random"'],
]),
},
])
)
})
it('should show params', async () => {
const browser = await next.browser('/params/next', { extraHTTPHeaders })
await browser.elementByCss('[data-react-server-requests-done]', {
state: 'attached',
})
const track = await browser.eval('window.reactServerRequests.getSnapshot()')
expect(track).toEqual(
expect.arrayContaining([
{
name: '\u200bparams [Prefetch]',
properties: [],
},
])
)
})
it('should show searchParams', async () => {
const browser = await next.browser('/searchparams?slug=next', {
extraHTTPHeaders,
})
await browser.elementByCss('[data-react-server-requests-done]', {
state: 'attached',
})
const track = await browser.eval('window.reactServerRequests.getSnapshot()')
expect(track).toEqual(
expect.arrayContaining([
{
name: '\u200bsearchParams [Prefetch]',
properties: [],
},
])
)
})
it('should show cookies', async () => {
const browser = await next.browser('/cookies', { extraHTTPHeaders })
await browser.elementByCss('[data-react-server-requests-done]', {
state: 'attached',
})
const track = await browser.eval('window.reactServerRequests.getSnapshot()')
expect(track).toEqual(
expect.arrayContaining([
{
name: '\u200bcookies [Prefetch]',
properties: [],
},
])
)
})
it('should show draftMode', async () => {
const browser = await next.browser('/draftMode', { extraHTTPHeaders })
await browser.elementByCss('[data-react-server-requests-done]', {
state: 'attached',
})
const track = await browser.eval('window.reactServerRequests.getSnapshot()')
expect(track).toEqual([])
})
it('should show headers', async () => {
const browser = await next.browser('/headers', { extraHTTPHeaders })
await browser.elementByCss('[data-react-server-requests-done]', {
state: 'attached',
})
const track = await browser.eval('window.reactServerRequests.getSnapshot()')
expect(track).toEqual(
expect.arrayContaining([
{
name: '\u200bheaders [Prefetch]',
properties: [],
},
])
)
})
})