Skip to content

Commit 2fbdac6

Browse files
authored
fix prefetch bailout detection for nested loading segments (#67358)
### What When PPR is off, app router prefetches will render the component tree up until it encounters a `loading` segment, at which point it'll just return some metadata about the segment and won't do any further rendering. This is an optimization to ensure prefetches are lightweight and don't potentially invoke expensive dynamic subtrees. However, there's a bug in this logic that is causing it to bail unexpectedly if a segment deeper in the tree contained a `loading.js` file. This would mean the loading state wouldn't be triggered until the second request for the full RSC data is initiated, resulting in an unintended delta between when a link is clicked and the loading state is shown. ### Why The `shouldSkipComponentTree` flag was incorrectly being set to `true` even if the `loading.js` segment appeared deeper in the tree. Prefetch requests from the client will always contain `FlightRouterState`, so the logic to check for loading deeper in the tree will always be missed. ### How This removes the `flightRouterState` check as it doesn't make sense: prefetches will currently _always_ include the `flightRouterState`, causing this to always short-circuit. I believe that this check is vestigial from when we were generating static `prefetch.rsc` outputs which is no longer the case. This mirrors a [similar check](https://github.com/vercel/next.js/blob/b87d8fc49983a3be568517d7ae14087749bb8ce3/packages/next/src/server/app-render/create-component-tree.tsx#L393) when determining if parallel route(s) should be rendered.
1 parent 7523f32 commit 2fbdac6

File tree

6 files changed

+40
-10
lines changed

6 files changed

+40
-10
lines changed

packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,15 @@ export async function walkTreeWithFlightRouterState({
111111
// Explicit refresh
112112
flightRouterState[3] === 'refetch'
113113

114+
// Pre-PPR, the `loading` component signals to the router how deep to render the component tree
115+
// to ensure prefetches are quick and inexpensive. If there's no `loading` component anywhere in the tree being rendered,
116+
// the prefetch will be short-circuited to avoid requesting a potentially very expensive subtree. If there's a `loading`
117+
// somewhere in the tree, we'll recursively render the component tree up until we encounter that loading component, and then stop.
114118
const shouldSkipComponentTree =
115-
// loading.tsx has no effect on prefetching when PPR is enabled
116119
!experimental.isRoutePPREnabled &&
117120
isPrefetch &&
118121
!Boolean(components.loading) &&
119-
(flightRouterState ||
120-
// If there is no flightRouterState, we need to check the entire loader tree, as otherwise we'll be only checking the root
121-
!hasLoadingComponentInTree(loaderTree))
122+
!hasLoadingComponentInTree(loaderTree)
122123

123124
if (!parentRendered && renderComponentsOnThisLevel) {
124125
const overriddenSegment =

test/e2e/app-dir/app-prefetch/app/page.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export default function HomePage() {
88
<Link href="/static-page" id="to-static-page">
99
To Static Page
1010
</Link>
11+
<Link href="/prefetch-auto/foobar" id="to-dynamic-page">
12+
To Dynamic Slug Page
13+
</Link>
1114
</>
1215
)
1316
}

test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/layout.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,26 @@ import Link from 'next/link'
22

33
export const dynamic = 'force-dynamic'
44

5+
function getData() {
6+
const res = new Promise((resolve) => {
7+
setTimeout(() => {
8+
resolve({ message: 'Layout Data!' })
9+
}, 2000)
10+
})
11+
return res
12+
}
13+
514
export default async function Layout({ children }) {
15+
const result = await getData()
16+
617
return (
718
<div>
819
<h1>Layout</h1>
920
<Link prefetch={undefined} href="/prefetch-auto/justputit">
1021
Prefetch Link
1122
</Link>
1223
{children}
24+
<h3>{JSON.stringify(result)}</h3>
1325
</div>
1426
)
1527
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export default function Loading() {
2-
return <h1>Loading Prefetch Auto</h1>
2+
return <h1 id="loading-text">Loading Prefetch Auto</h1>
33
}

test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/page.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const dynamic = 'force-dynamic'
33
function getData() {
44
const res = new Promise((resolve) => {
55
setTimeout(() => {
6-
resolve({ message: 'Hello World!' })
6+
resolve({ message: 'Page Data!' })
77
}, 2000)
88
})
99
return res
@@ -13,9 +13,9 @@ export default async function Page({ params }) {
1313
const result = await getData()
1414

1515
return (
16-
<>
16+
<div id="prefetch-auto-page-data">
1717
<h3>{JSON.stringify(params)}</h3>
1818
<h3>{JSON.stringify(result)}</h3>
19-
</>
19+
</div>
2020
)
2121
}

test/e2e/app-dir/app-prefetch/prefetching.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ describe('app dir - prefetching', () => {
220220
})
221221

222222
const prefetchResponse = await response.text()
223-
expect(prefetchResponse).not.toContain('Hello World')
223+
expect(prefetchResponse).toContain('Page Data!')
224+
expect(prefetchResponse).not.toContain('Layout Data!')
224225
expect(prefetchResponse).not.toContain('Loading Prefetch Auto')
225226
})
226227

@@ -254,7 +255,7 @@ describe('app dir - prefetching', () => {
254255
})
255256

256257
const prefetchResponse = await response.text()
257-
expect(prefetchResponse).not.toContain('Hello World')
258+
expect(prefetchResponse).not.toContain('Page Data!')
258259
expect(prefetchResponse).toContain('Loading Prefetch Auto')
259260
})
260261

@@ -275,6 +276,19 @@ describe('app dir - prefetching', () => {
275276
)
276277
})
277278

279+
it('should immediately render the loading state for a dynamic segment when fetched from higher up in the tree', async () => {
280+
const browser = await next.browser('/')
281+
const loadingText = await browser
282+
.elementById('to-dynamic-page')
283+
.click()
284+
.waitForElementByCss('#loading-text')
285+
.text()
286+
287+
expect(loadingText).toBe('Loading Prefetch Auto')
288+
289+
await browser.waitForElementByCss('#prefetch-auto-page-data')
290+
})
291+
278292
describe('dynamic rendering', () => {
279293
describe.each(['/force-dynamic', '/revalidate-0'])('%s', (basePath) => {
280294
it('should not re-render layout when navigating between sub-pages', async () => {

0 commit comments

Comments
 (0)