Skip to content

Commit 3043fee

Browse files
disable static prefetching behavior for dynamic segments (vercel#58609)
### What? When a layout segment forces dynamic rendering (such as with `force-dynamic` or `revalidate: 0`), navigating to sub-pages of that layout will attempt to re-render the layout, also resulting in side effects re-running. This means if your layout relies on a data fetch and you render the result of that data in the layout, it will unexpectedly change when navigating between sub-paths, as described in vercel#57326. As a separate issue (but caused by the same underlying mechanism), when using `searchParams` on a dynamic page, changes to those search params will be erroneously ignored when navigating, as described in vercel#57075 ### Why? As a performance optimization we generate static prefetch files for dynamic segments ([original PR](vercel#54403)). This makes it so that when prefetching is turned on, the prefetch can be served quickly from the edge without needing to invoke unnecessarily. We're able to eagerly serve things that can be safely prefetched. This is nice for cases where a path has a `loading.js` that we can eagerly render while waiting for the dynamic data to be loaded. This causes a problem with layouts that opt into dynamic rendering: when the page loads and a prefetch is kicked off for the sub-page, it'll load the static prefetch, which won't be generated with the same router state as the dynamically rendered page. This causes a mismatch between the two trees, and when navigating within the same segment, a refetch will be added to the router because it thinks that it's navigating to a new layout. This also causes issues for dynamic pages that use `searchParams`. The static prefetch will be generated without any knowledge of search params, and when the prefetch occurs, we still match to the prefetch generated without search params. This will make the router think that no change occurs, and the UI will not update to reflect the change. ### How? There's ongoing work by @acdlite to refactor the client router. Hopefully it will be easier to re-land this once that work is finished. For now, I've reverted the behavior as it doesn't seem to be worth the bugs it currently causes. I've also added tests so that when we do re-land this behavior, we can catch these subtleties. Fixes vercel#57326 Fixes vercel#57075 Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent f6b50ae commit 3043fee

File tree

13 files changed

+229
-62
lines changed

13 files changed

+229
-62
lines changed

packages/next/src/export/routes/app-page.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@ async function generatePrefetchRsc(
3737
htmlFilepath: string,
3838
renderOpts: RenderOpts,
3939
fileWriter: FileWriter
40-
) {
40+
): Promise<boolean> {
41+
// TODO: Re-enable once this is better supported client-side
42+
// It's currently not reliable to generate these prefetches because the client router
43+
// depends on the RSC payload being generated with FlightRouterState. When we generate these prefetches
44+
// without router state, it causes mismatches on client-side nav, resulting in subtle navigation bugs
45+
// like unnecessarily re-rendering layouts.
46+
return false
47+
4148
// When we're in PPR, the RSC payload is emitted as the prefetch payload, so
4249
// attempting to generate a prefetch RSC is an error.
4350
if (renderOpts.experimental.ppr) {
@@ -64,13 +71,15 @@ async function generatePrefetchRsc(
6471

6572
const prefetchRscData = await prefetchRenderResult.toUnchunkedString(true)
6673

67-
if ((renderOpts as any).store.staticPrefetchBailout) return
74+
if ((renderOpts as any).store.staticPrefetchBailout) return false
6875

6976
await fileWriter(
7077
ExportedAppPageFiles.FLIGHT,
7178
htmlFilepath.replace(/\.html$/, RSC_PREFETCH_SUFFIX),
7279
prefetchRscData
7380
)
81+
82+
return true
7483
}
7584

7685
export async function exportAppPage(
@@ -94,7 +103,7 @@ export async function exportAppPage(
94103

95104
try {
96105
if (isAppPrefetch) {
97-
await generatePrefetchRsc(
106+
const generated = await generatePrefetchRsc(
98107
req,
99108
path,
100109
res,
@@ -104,7 +113,9 @@ export async function exportAppPage(
104113
fileWriter
105114
)
106115

107-
return { revalidate: 0 }
116+
if (generated) {
117+
return { revalidate: 0 }
118+
}
108119
}
109120

110121
const result = await lazyRenderAppPage(

packages/next/src/server/base-server.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ import {
8484
NEXT_RSC_UNION_QUERY,
8585
ACTION,
8686
NEXT_ROUTER_PREFETCH_HEADER,
87-
RSC_CONTENT_TYPE_HEADER,
8887
} from '../client/components/app-router-headers'
8988
import type {
9089
MatchOptions,
@@ -2315,28 +2314,29 @@ export default abstract class Server<ServerOptions extends Options = Options> {
23152314
{ page: pathname, params: opts.params, query, renderOpts }
23162315
)
23172316
} else if (isAppPageRouteModule(routeModule)) {
2318-
if (
2319-
!opts.experimental.ppr &&
2320-
isPrefetchRSCRequest &&
2321-
process.env.NODE_ENV === 'production' &&
2322-
!this.minimalMode
2323-
) {
2324-
try {
2325-
const prefetchRsc = await this.getPrefetchRsc(resolvedUrlPathname)
2326-
if (prefetchRsc) {
2327-
res.setHeader(
2328-
'cache-control',
2329-
'private, no-cache, no-store, max-age=0, must-revalidate'
2330-
)
2331-
res.setHeader('content-type', RSC_CONTENT_TYPE_HEADER)
2332-
res.body(prefetchRsc).send()
2333-
return null
2334-
}
2335-
} catch {
2336-
// We fallback to invoking the function if prefetch data is not
2337-
// available.
2338-
}
2339-
}
2317+
// TODO: Re-enable once static prefetches re-land
2318+
// if (
2319+
// !opts.experimental.ppr &&
2320+
// isPrefetchRSCRequest &&
2321+
// process.env.NODE_ENV === 'production' &&
2322+
// !this.minimalMode
2323+
// ) {
2324+
// try {
2325+
// const prefetchRsc = await this.getPrefetchRsc(resolvedUrlPathname)
2326+
// if (prefetchRsc) {
2327+
// res.setHeader(
2328+
// 'cache-control',
2329+
// 'private, no-cache, no-store, max-age=0, must-revalidate'
2330+
// )
2331+
// res.setHeader('content-type', RSC_CONTENT_TYPE_HEADER)
2332+
// res.body(prefetchRsc).send()
2333+
// return null
2334+
// }
2335+
// } catch {
2336+
// // We fallback to invoking the function if prefetch data is not
2337+
// // available.
2338+
// }
2339+
// }
23402340

23412341
const module = components.routeModule as AppPageRouteModule
23422342

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react'
2+
3+
export const dynamic = 'force-dynamic'
4+
5+
export default async function Layout({ children }) {
6+
console.log('re-fetching in layout')
7+
const data = await fetch(
8+
'https://next-data-api-endpoint.vercel.app/api/random'
9+
)
10+
const randomNumber = await data.text()
11+
12+
return (
13+
<div>
14+
<p id="random-number">{randomNumber}</p>
15+
16+
{children}
17+
</div>
18+
)
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Link from 'next/link'
2+
3+
export default function Home({ searchParams }) {
4+
return (
5+
<>
6+
<div id="search-params-data">{JSON.stringify(searchParams)}</div>
7+
<Link href="?foo=true">Add search params</Link>
8+
<Link href="/force-dynamic/search-params">Clear Params</Link>
9+
</>
10+
)
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Link from 'next/link'
2+
3+
export default function Page() {
4+
return (
5+
<div id="test-page">
6+
Hello from /force-dynamic/test-page{' '}
7+
<Link href="/force-dynamic/test-page/sub-page">To Sub Page</Link>
8+
</div>
9+
)
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Link from 'next/link'
2+
3+
export default function Page() {
4+
return (
5+
<div id="sub-page">
6+
Hello from /force-dynamic/test-page/sub-page{' '}
7+
<Link href="/force-dynamic/test-page">Back to Test Page</Link>
8+
</div>
9+
)
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react'
2+
3+
export const revalidate = 0
4+
5+
export default async function Layout({ children }) {
6+
console.log('re-fetching in layout')
7+
const data = await fetch(
8+
'https://next-data-api-endpoint.vercel.app/api/random'
9+
)
10+
const randomNumber = await data.text()
11+
12+
return (
13+
<div>
14+
<p id="random-number">{randomNumber}</p>
15+
16+
{children}
17+
</div>
18+
)
19+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Link from 'next/link'
2+
3+
export default function Home({ searchParams }) {
4+
return (
5+
<>
6+
<div id="search-params-data">{JSON.stringify(searchParams)}</div>
7+
<Link href="?foo=true">Add search params</Link>
8+
<Link href="/revalidate-0/search-params">Clear Params</Link>
9+
</>
10+
)
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Link from 'next/link'
2+
3+
export default function Page() {
4+
return (
5+
<div id="test-page">
6+
Hello from /revalidate-0/test-page{' '}
7+
<Link href="/revalidate-0/test-page/sub-page">To Sub Page</Link>
8+
</div>
9+
)
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Link from 'next/link'
2+
3+
export default function Page() {
4+
return (
5+
<div id="sub-page">
6+
Hello from /revalidate-0/test-page/sub-page{' '}
7+
<Link href="/revalidate-0/test-page">Back to Test Page</Link>
8+
</div>
9+
)
10+
}

0 commit comments

Comments
 (0)