Skip to content

Commit ce5206f

Browse files
authoredApr 4, 2025··
[dynamicIO] Fix dev warmup (#77829)
When `dynamicIO` is enabled, we're triggering a warmup request in dev mode. This ensures that replayed logs are associated with the correct environment (`Prerender` vs. `Server`), by seeding the caches before the actual render. This PR fixes two issues with the dev warmup: - Ensures that cache keys are identical between the warmup, the subsequent dynamic render, and the dynamic validation, by providing the HMR refresh hash (part of the cache key) for all dev render phases. - Ensures that stale cache entries are discarded during the warmup, by providing the implicit tags also during the warmup (and the dynamic validation).
1 parent f1312ef commit ce5206f

File tree

9 files changed

+150
-66
lines changed

9 files changed

+150
-66
lines changed
 

‎packages/next/src/client/components/app-router-headers.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const NEXT_ROUTER_PREFETCH_HEADER = 'Next-Router-Prefetch' as const
1212
export const NEXT_ROUTER_SEGMENT_PREFETCH_HEADER =
1313
'Next-Router-Segment-Prefetch' as const
1414
export const NEXT_HMR_REFRESH_HEADER = 'Next-HMR-Refresh' as const
15+
export const NEXT_HMR_REFRESH_HASH_COOKIE = '__next_hmr_refresh_hash__' as const
1516
export const NEXT_URL = 'Next-Url' as const
1617
export const RSC_CONTENT_TYPE_HEADER = 'text/x-component' as const
1718

‎packages/next/src/client/components/react-dev-overlay/app/hot-reloader-client.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import type { GlobalErrorComponent } from '../../error-boundary'
4646
import type { DevIndicatorServerState } from '../../../../server/dev/dev-indicator-server-state'
4747
import reportHmrLatency from '../utils/report-hmr-latency'
4848
import { TurbopackHmr } from '../utils/turbopack-hot-reloader-common'
49+
import { NEXT_HMR_REFRESH_HASH_COOKIE } from '../../app-router-headers'
4950

5051
export interface Dispatcher {
5152
onBuildOk(): void
@@ -412,7 +413,7 @@ function processMessage(
412413

413414
// Store the latest hash in a session cookie so that it's sent back to the
414415
// server with any subsequent requests.
415-
document.cookie = `__next_hmr_refresh_hash__=${obj.hash}`
416+
document.cookie = `${NEXT_HMR_REFRESH_HASH_COOKIE}=${obj.hash}`
416417

417418
if (RuntimeErrorHandler.hadRuntimeError) {
418419
if (reloading) return

‎packages/next/src/server/app-render/app-render.tsx

+89-50
Large diffs are not rendered by default.

‎packages/next/src/server/app-render/work-unit-async-storage.external.ts

+22-5
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ import type {
1616
import type { Params } from '../request/params'
1717
import type { ImplicitTags } from '../lib/implicit-tags'
1818
import type { WorkStore } from './work-async-storage.external'
19+
import { NEXT_HMR_REFRESH_HASH_COOKIE } from '../../client/components/app-router-headers'
1920

2021
export type WorkUnitPhase = 'action' | 'render' | 'after'
2122

2223
export interface CommonWorkUnitStore {
2324
/** NOTE: Will be mutated as phases change */
2425
phase: WorkUnitPhase
25-
readonly implicitTags: ImplicitTags | undefined
26+
readonly implicitTags: ImplicitTags
2627
}
2728

2829
export interface RequestStore extends CommonWorkUnitStore {
@@ -121,6 +122,13 @@ export interface PrerenderStoreModern extends CommonWorkUnitStore {
121122
// not part of the primary render path and are just prerendering to produce
122123
// validation results
123124
validating?: boolean
125+
126+
/**
127+
* The HMR refresh hash is only provided in dev mode. It is needed for the dev
128+
* warmup render to ensure that the cache keys will be identical for the
129+
* subsequent dynamic render.
130+
*/
131+
readonly hmrRefreshHash: string | undefined
124132
}
125133

126134
export interface PrerenderStorePPR extends CommonWorkUnitStore {
@@ -154,7 +162,16 @@ export type PrerenderStore =
154162
| PrerenderStorePPR
155163
| PrerenderStoreModern
156164

157-
export interface UseCacheStore extends CommonWorkUnitStore {
165+
export interface CommonCacheStore
166+
extends Omit<CommonWorkUnitStore, 'implicitTags'> {
167+
/**
168+
* A cache work unit store might not always have an outer work unit store,
169+
* from which implicit tags could be inherited.
170+
*/
171+
readonly implicitTags: ImplicitTags | undefined
172+
}
173+
174+
export interface UseCacheStore extends CommonCacheStore {
158175
type: 'cache'
159176
// Collected revalidate times and tags for this cache entry during the cache render.
160177
revalidate: number // implicit revalidate time from inner caches / fetches
@@ -173,7 +190,7 @@ export interface UseCacheStore extends CommonWorkUnitStore {
173190
readonly draftMode: DraftModeProvider | undefined
174191
}
175192

176-
export interface UnstableCacheStore extends CommonWorkUnitStore {
193+
export interface UnstableCacheStore extends CommonCacheStore {
177194
type: 'unstable-cache'
178195
// Draft mode is only available if the outer work unit store is a request
179196
// store and draft mode is enabled.
@@ -276,10 +293,10 @@ export function getHmrRefreshHash(
276293
return undefined
277294
}
278295

279-
return workUnitStore.type === 'cache'
296+
return workUnitStore.type === 'cache' || workUnitStore.type === 'prerender'
280297
? workUnitStore.hmrRefreshHash
281298
: workUnitStore.type === 'request'
282-
? workUnitStore.cookies.get('__next_hmr_refresh_hash__')?.value
299+
? workUnitStore.cookies.get(NEXT_HMR_REFRESH_HASH_COOKIE)?.value
283300
: undefined
284301
}
285302

‎packages/next/src/server/async-storage/request-store.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ type RequestContext = RequestResponsePair & {
6868
renderOpts?: WrapperRenderOpts
6969
isHmrRefresh?: boolean
7070
serverComponentsHmrCache?: ServerComponentsHmrCache
71-
implicitTags: ImplicitTags | undefined
71+
implicitTags: ImplicitTags
7272
}
7373

7474
type RequestResponsePair =

‎packages/next/src/server/route-modules/app-route/module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ export class AppRouteRouteModule extends RouteModule<
393393
stale: INFINITE_CACHE,
394394
tags: [...implicitTags.tags],
395395
prerenderResumeDataCache: null,
396+
hmrRefreshHash: undefined,
396397
})
397398

398399
let prospectiveResult
@@ -478,6 +479,7 @@ export class AppRouteRouteModule extends RouteModule<
478479
stale: INFINITE_CACHE,
479480
tags: [...implicitTags.tags],
480481
prerenderResumeDataCache: null,
482+
hmrRefreshHash: undefined,
481483
})
482484

483485
let responseHandled = false

‎packages/next/src/server/web/adapter.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { MiddlewareSpan } from '../lib/trace/constants'
3333
import { CloseController } from './web-on-close'
3434
import { getEdgePreviewProps } from './get-edge-preview-props'
3535
import { getBuiltinRequestContext } from '../after/builtin-request-context'
36+
import { getImplicitTags } from '../lib/implicit-tags'
3637

3738
export class NextRequestHint extends NextRequest {
3839
sourcePage: string
@@ -251,18 +252,26 @@ export async function adapter(
251252
cookiesFromResponse = cookies
252253
}
253254
const previewProps = getEdgePreviewProps()
255+
const page = '/' // Fake Work
256+
const fallbackRouteParams = null
257+
258+
const implicitTags = await getImplicitTags(
259+
page,
260+
request.nextUrl,
261+
fallbackRouteParams
262+
)
254263

255264
const requestStore = createRequestStoreForAPI(
256265
request,
257266
request.nextUrl,
258-
undefined,
267+
implicitTags,
259268
onUpdateCookies,
260269
previewProps
261270
)
262271

263272
const workStore = createWorkStore({
264-
page: '/', // Fake Work
265-
fallbackRouteParams: null,
273+
page,
274+
fallbackRouteParams,
266275
renderOpts: {
267276
cacheLifeProfiles:
268277
params.request.nextConfig?.experimental?.cacheLife,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { revalidatePath } from 'next/cache'
2+
3+
export async function GET() {
4+
revalidatePath('/')
5+
6+
return Response.json({ revalidated: true })
7+
}

‎test/development/app-dir/dynamic-io-dev-warmup/dynamic-io.dev-warmup.test.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,21 @@ describe('dynamic-io-dev-warmup', () => {
2121

2222
it('logs with Prerender or Server environment depending based on whether the timing of when the log runs relative to this environment boundary', async () => {
2323
let browser = await next.browser('/')
24-
// At the moment this second render is required for the logs to resolves in the expected environment
25-
// This doesn't reproduce locally but I suspect some kind of lazy initialization during dev that leads the initial render
26-
// to not resolve in a microtask on the first render.
27-
await browser.close()
28-
browser = await next.browser('/')
24+
let logs = await browser.log()
25+
26+
assertLog(logs, 'after layout cache read', 'Prerender')
27+
assertLog(logs, 'after page cache read', 'Prerender')
28+
assertLog(logs, 'after cached layout fetch', 'Prerender')
29+
assertLog(logs, 'after cached page fetch', 'Prerender')
30+
assertLog(logs, 'after uncached layout fetch', 'Server')
31+
assertLog(logs, 'after uncached page fetch', 'Server')
2932

30-
const logs = await browser.log()
33+
// After a revalidation the subsequent warmup render must discard stale
34+
// cache entries.
35+
await next.fetch('/revalidate')
36+
37+
browser = await next.browser('/')
38+
logs = await browser.log()
3139

3240
assertLog(logs, 'after layout cache read', 'Prerender')
3341
assertLog(logs, 'after page cache read', 'Prerender')

0 commit comments

Comments
 (0)
Please sign in to comment.