Skip to content

[Segment Cache] Support dynamic head prefetching #81677

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions packages/next/src/client/components/segment-cache-impl/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,16 @@ function writeDynamicRenderResponseIntoCache(
// TODO: We should cache this, too, so that the MPA navigation is immediate.
return null
}

const staleTimeHeaderSeconds = response.headers.get(
NEXT_ROUTER_STALE_TIME_HEADER
)
const staleTimeMs =
staleTimeHeaderSeconds !== null
? parseInt(staleTimeHeaderSeconds, 10) * 1000
: STATIC_STALETIME_MS
const staleAt = now + staleTimeMs

for (const flightData of flightDatas) {
const seedData = flightData.seedData
if (seedData !== null) {
Expand All @@ -1555,24 +1565,36 @@ function writeDynamicRenderResponseIntoCache(
encodeSegment(segment)
)
}
const staleTimeHeaderSeconds = response.headers.get(
NEXT_ROUTER_STALE_TIME_HEADER
)
const staleTimeMs =
staleTimeHeaderSeconds !== null
? parseInt(staleTimeHeaderSeconds, 10) * 1000
: STATIC_STALETIME_MS

writeSeedDataIntoCache(
now,
task,
route,
now + staleTimeMs,
staleAt,
seedData,
isResponsePartial,
segmentKey,
spawnedEntries
)
}

// During a dynamic request, the server sends back new head data for the
// page. Overwrite the existing head with the new one. Note that we're
// intentionally not taking into account whether the existing head is
// already complete, even though the incoming head might not have finished
// streaming in yet. This is to prioritize consistency of the head with
// the segment data (though it's still not a guarantee, since some of the
// segment data may be reused from a previous request).
route.head = flightData.head
route.isHeadPartial = flightData.isHeadPartial
// TODO: Currently the stale time of the route tree represents the
// stale time of both the route tree *and* all the segment data. So we
// can't just overwrite this field; we have to use whichever value is
// lower. In the future, though, the plan is to track segment lifetimes
// separately from the route tree lifetime.
if (staleAt < route.staleAt) {
route.staleAt = staleAt
}
}
// Any entry that's still pending was intentionally not rendered by the
// server, because it was inside the loading boundary. Mark them as rejected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ export default function Page() {
</li>
</ul>
</li>
<li>
<LinkAccordion
prefetch={true}
href="/ppr-disabled-dynamic-head?foo=yay"
>
Page with PPR disabled and a dynamic head, prefetch=true
</LinkAccordion>
</li>
</ul>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Suspense } from 'react'
import { connection } from 'next/server'
import { Metadata } from 'next'

export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<{ foo: string }>
}): Promise<Metadata> {
const { foo } = await searchParams
return {
title: 'Dynamic Title: ' + (foo ?? '(empty)'),
}
}

async function Content() {
await connection()
return <div id="page-content">Page content</div>
}

export default function PPRDisabledDynamicHead() {
return (
<Suspense fallback="Loading...">
<Content />
</Suspense>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -475,4 +475,41 @@ describe('segment cache (incremental opt in)', () => {
)
}
)

it('fully prefetch a page with a dynamic title', async () => {
let act
const browser = await next.browser('/', {
beforePageLoad(p) {
act = createRouterAct(p)
},
})

await act(
async () => {
const checkbox = await browser.elementByCss(
'input[data-link-accordion="/ppr-disabled-dynamic-head?foo=yay"]'
)
await checkbox.click()
},
// Because the link is prefetched with prefetch=true, we should be able to
// prefetch the title, even though it's dynamic.
{
includes: 'Dynamic Title: yay',
}
)

// When we navigate to the page, it should not make any additional
// network requests, because both the segment data and the head were
// fully prefetched.
await act(async () => {
const link = await browser.elementByCss(
'a[href="/ppr-disabled-dynamic-head?foo=yay"]'
)
await link.click()
const pageContent = await browser.elementById('page-content')
expect(await pageContent.text()).toBe('Page content')
const title = await browser.eval(() => document.title)
expect(title).toBe('Dynamic Title: yay')
}, 'no-requests')
})
})
Loading