Files
next.js/test/development/app-dir/react-performance-track/react-performance-track.test.ts
Josh StoryandGitHub ea3cad2848 Only error for sync IO after runtime in segments that would be runtime prefetched (#89979)
Stacked on #89984

When we initially implemented runtime prefetching we decided to make
sync IO that happens after runtime but before async IO an error. This is
similar to but more aggressive than the error for sync IO before runtime
data but is important because if you have sync IO before uncached IO and
we are runtime prefetching the page then we aren't going to get anything
useful from that prefetch but it might be very expensive.

The problem with the current implementation is that if you turn on
runtime prefetching for a Page segment then in practice the router will
only ever runtime prefetch that page segment but the sync IO validator
will error for sync IO before uncached IO anywhere in the route. This
makes adding runtime prefetching to leaf Segments much harder b/c you
have to address sync IO in the entire Segment stack.

This change updates the sync IO heuristic to only error in segments that
would be runtime prefetched and below. This way when you add runtime
prefetching to a segment you only have to true up the sync IO for the
part of the tree that has the changed behavior.

This PR only partially implements the intended semantics for runtime
prefetching validation of sync IO. One thing that will need to be done
in a follow up is handling sync IO in generateMetadata. In this case
while the functions are defined in layouts and pages they are
semantically always "owned" by the page itself. This means that while
the layout might not be runtime prefetchable and thus allow sync IO
after runtime data access the generateMetadata in that same layout might
not allow sync IO after runtime if there is a runtime prefetch config at
the page level or some layout higher. This will be done in a follow up
2026-02-18 14:49:30 -08:00

152 lines
4.6 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 [Prefetchable]',
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 [Prefetchable]',
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 [Prefetchable]',
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()')
// TODO the addition of a promise to delay some Segments from rendering until the later Static
// stage has caused the draftMode snapshot to include an empty-named entry. This is probably
// a bug in React and should be fixed there but
// expect(track).toEqual([])
expect(track).toEqual(
expect.arrayContaining([
{
name: '\u200b [Prerender]',
properties: [],
},
])
)
let didThrow = false
try {
// including this anti-assertion here so we can restore the test when the bug in React is fixed
expect(track).toEqual([])
} catch (e) {
didThrow = true
}
expect(didThrow).toBe(true)
})
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 [Prefetchable]',
properties: [],
},
])
)
})
})