From fd5f4e102c4312abd919265ea35bea811dbde0bf Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Wed, 3 Jun 2026 21:40:15 +0200 Subject: [PATCH 1/8] [CC] add shell stages (#94438) Introduces new shell stages and adds relevant tasks where needed. We're not changing the timing of when anything resolves yet, so this should not have any behavioral changes. --- packages/next/src/lib/metadata/metadata.tsx | 10 +- .../next/src/server/app-render/app-render.tsx | 423 ++++++++++-------- .../app-render/create-component-tree.tsx | 18 +- .../src/server/app-render/staged-rendering.ts | 60 ++- 4 files changed, 316 insertions(+), 195 deletions(-) diff --git a/packages/next/src/lib/metadata/metadata.tsx b/packages/next/src/lib/metadata/metadata.tsx index 48c0bdca81e6..bb6fae207cc7 100644 --- a/packages/next/src/lib/metadata/metadata.tsx +++ b/packages/next/src/lib/metadata/metadata.tsx @@ -21,7 +21,6 @@ import { workUnitAsyncStorage, getStagedRenderingController, } from '../../server/app-render/work-unit-async-storage.external' -import { RenderStage } from '../../server/app-render/staged-rendering' import { MetadataBoundary, @@ -31,6 +30,7 @@ import { import { getOrigin } from './generate/utils' import { IconMark } from './generate/icon-mark' +import { FIRST_LATE_RENDER_STAGE } from '../../server/app-render/staged-rendering' // Use a promise to share the status of the metadata resolving, // returning two components `MetadataTree` and `MetadataOutlet` @@ -72,14 +72,14 @@ export function createMetadataComponents({ async function Viewport() { // Gate metadata to the correct render stage. If the page is not - // runtime-prefetchable, defer until the Static stage so that + // runtime-prefetchable, defer until the ShellStatic stage so that // prefetchable segments get a head start. if (!isRuntimePrefetchable) { const workUnitStore = workUnitAsyncStorage.getStore() if (workUnitStore) { const stagedRendering = getStagedRenderingController(workUnitStore) if (stagedRendering) { - await stagedRendering.waitForStage(RenderStage.Static) + await stagedRendering.waitForStage(FIRST_LATE_RENDER_STAGE) } } } @@ -123,14 +123,14 @@ export function createMetadataComponents({ async function Metadata() { // Gate metadata to the correct render stage. If the page is not - // runtime-prefetchable, defer until the Static stage so that + // runtime-prefetchable, defer until the ShellStatic stage so that // prefetchable segments get a head start. if (!isRuntimePrefetchable) { const workUnitStore = workUnitAsyncStorage.getStore() if (workUnitStore) { const stagedRendering = getStagedRenderingController(workUnitStore) if (stagedRendering) { - await stagedRendering.waitForStage(RenderStage.Static) + await stagedRendering.waitForStage(FIRST_LATE_RENDER_STAGE) } } } diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 1961111e2c35..9bb2ab91ad96 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -1014,7 +1014,8 @@ async function generateStagedDynamicFlightRenderResultWeb( const flightReadableStream = await runInSequentialTasks( () => { - stageController.advanceStage(RenderStage.Static) + // NOTE: no early/late separation in this render + stageController.advanceStage(RenderStage.ShellStatic) const stream = workUnitAsyncStorage.run( requestStore, @@ -1032,6 +1033,9 @@ async function generateStagedDynamicFlightRenderResultWeb( return dynamicStream }, + () => { + stageController.advanceStage(RenderStage.Static) + }, () => { // This is a separate task that doesn't advance a stage. It forces // draining the immediate queue so that the stale time iterable and vary @@ -1173,7 +1177,8 @@ async function generateStagedDynamicFlightRenderResultNode( const flightStream = await runInSequentialTasks( () => { - stageController.advanceStage(RenderStage.Static) + // NOTE: no early/late separation in this render + stageController.advanceStage(RenderStage.ShellStatic) const sourceStream = workUnitAsyncStorage.run( requestStore, @@ -1194,6 +1199,9 @@ async function generateStagedDynamicFlightRenderResultNode( return dynamicStream }, + () => { + stageController.advanceStage(RenderStage.Static) + }, () => { // This is a separate task that doesn't advance a stage. It forces // draining the immediate queue so that the stale time iterable and vary @@ -1285,20 +1293,7 @@ async function stagedRenderWithoutCachesInDevWeb( const environmentName = () => { const currentStage = stageController.currentStage - switch (currentStage) { - case RenderStage.Before: - case RenderStage.EarlyStatic: - case RenderStage.Static: - return 'Prerender' - case RenderStage.EarlyRuntime: - case RenderStage.Runtime: - case RenderStage.Dynamic: - case RenderStage.Abandoned: - return 'Server' - default: - currentStage satisfies never - throw new InvariantError(`Invalid render stage: ${currentStage}`) - } + return getEnvironmentNameForStageWithoutCaches(currentStage) } requestStore.stagedRendering = stageController @@ -1314,7 +1309,9 @@ async function stagedRenderWithoutCachesInDevWeb( return await runInSequentialTasks( () => { - stageController.advanceStage(RenderStage.Static) + // NOTE: no early/late separation in this render + stageController.advanceStage(RenderStage.ShellStatic) + return workUnitAsyncStorage.run( requestStore, renderToWebFlightStream, @@ -1327,6 +1324,9 @@ async function stagedRenderWithoutCachesInDevWeb( } ) }, + () => { + stageController.advanceStage(RenderStage.Static) + }, () => { stageController.advanceStage(RenderStage.Dynamic) } @@ -1356,20 +1356,7 @@ async function stagedRenderWithoutCachesInDevNode( const environmentName = () => { const currentStage = stageController.currentStage - switch (currentStage) { - case RenderStage.Before: - case RenderStage.EarlyStatic: - case RenderStage.Static: - return 'Prerender' - case RenderStage.EarlyRuntime: - case RenderStage.Runtime: - case RenderStage.Dynamic: - case RenderStage.Abandoned: - return 'Server' - default: - currentStage satisfies never - throw new InvariantError(`Invalid render stage: ${currentStage}`) - } + return getEnvironmentNameForStageWithoutCaches(currentStage) } requestStore.stagedRendering = stageController @@ -1385,7 +1372,9 @@ async function stagedRenderWithoutCachesInDevNode( return await runInSequentialTasks( () => { - stageController.advanceStage(RenderStage.Static) + // NOTE: no early/late separation in this render + stageController.advanceStage(RenderStage.ShellStatic) + return workUnitAsyncStorage.run( requestStore, renderToNodeFlightStream, @@ -1398,12 +1387,36 @@ async function stagedRenderWithoutCachesInDevNode( } ) }, + () => { + stageController.advanceStage(RenderStage.Static) + }, () => { stageController.advanceStage(RenderStage.Dynamic) } ) } +function getEnvironmentNameForStageWithoutCaches(stage: RenderStage) { + switch (stage) { + case RenderStage.Before: + case RenderStage.ShellEarlyStatic: + case RenderStage.ShellStatic: + case RenderStage.EarlyStatic: + case RenderStage.Static: + return 'Prerender' + case RenderStage.ShellEarlyRuntime: + case RenderStage.ShellRuntime: + case RenderStage.EarlyRuntime: + case RenderStage.Runtime: + case RenderStage.Dynamic: + case RenderStage.Abandoned: + return 'Server' + default: + stage satisfies never + throw new InvariantError(`Invalid render stage: ${stage}`) + } +} + /** * Fork of `generateDynamicFlightRenderResult` that renders using `renderWithRestartOnCacheMissInDev` * to ensure correct separation of environments Prerender/Server (for use in Cache Components) @@ -1932,10 +1945,10 @@ async function finalRuntimeServerPrerender( await runInSequentialTasks( async () => { - // EarlyStatic stage: render begins. - // Runtime-prefetchable segments render immediately. - // Non-prefetchable segments are gated until the Static stage. - finalStageController.advanceStage(RenderStage.EarlyStatic) + // Runtime-prefetchable segments render immediately in the early stage. + // Non-prefetchable segments are gated until the first late stage. + finalStageController.advanceStage(RenderStage.ShellEarlyStatic) + const stream = workUnitAsyncStorage.run( finalServerPrerenderStore, ComponentMod.renderToReadableStream, @@ -1958,18 +1971,35 @@ async function finalRuntimeServerPrerender( ) }, () => { - // Advance to Static stage: resolve promise holding back - // non-prefetchable segments so they can begin rendering. + // Resolve the promise holding back non-prefetchable segments so they can begin rendering. + finalStageController.advanceStage(RenderStage.ShellStatic) + }, + () => { + finalStageController.advanceStage(RenderStage.EarlyStatic) + }, + () => { finalStageController.advanceStage(RenderStage.Static) }, () => { - // Advance to EarlyRuntime stage: resolve cookies/headers for - // runtime-prefetchable segments. Sync IO is checked here. + // TODO(app-shells): resolve session data for runtime-prefetchable segments here + // Sync IO is NOT allowed here. + finalStageController.advanceStage(RenderStage.ShellEarlyRuntime) + }, + () => { + // TODO(app-shells): resolve session data here for non-prefetchable segments here + // Sync IO is allowed here. + finalStageController.advanceStage(RenderStage.ShellRuntime) + }, + () => { + // TODO(app-shells): resolve link data for runtime-prefetchable segments here + // Resolve runtime data for runtime-prefetchable segments. + // Sync IO is NOT allowed here. finalStageController.advanceStage(RenderStage.EarlyRuntime) }, () => { - // Advance to Runtime stage: resolve cookies/headers for - // non-prefetchable segments. Sync IO is allowed here. + // TODO(app-shells): resolve link data for runtime-prefetchable segments here + // Resolve runtime data for non-prefetchable segments. + // Sync IO is allowed here. finalStageController.advanceStage(RenderStage.Runtime) }, async () => { @@ -3788,7 +3818,8 @@ async function renderToStream( const flightStream = await runInSequentialTasks( () => { - stageController.advanceStage(RenderStage.Static) + // NOTE: no early/late separation in this render + stageController.advanceStage(RenderStage.ShellStatic) const stream = workUnitAsyncStorage.run( requestStore, @@ -3812,6 +3843,9 @@ async function renderToStream( return dynamicStream }, + () => { + stageController.advanceStage(RenderStage.Static) + }, () => { // This is a separate task that doesn't advance a stage. It forces // draining the immediate queue so that the stale time iterable and vary @@ -3920,7 +3954,8 @@ async function renderToStream( const flightStream = await runInSequentialTasks( () => { - stageController.advanceStage(RenderStage.Static) + // NOTE: no early/late separation in this render + stageController.advanceStage(RenderStage.ShellStatic) const stream = workUnitAsyncStorage.run( requestStore, @@ -3941,6 +3976,9 @@ async function renderToStream( return dynamicStream }, + () => { + stageController.advanceStage(RenderStage.Static) + }, () => { // This is a separate task that doesn't advance a stage. It forces // draining the immediate queue so that the stale time iterable and vary @@ -4620,22 +4658,7 @@ async function renderWithRestartOnCacheMissInDevWeb( const environmentName = () => { const currentStage = requestStore.stagedRendering!.currentStage - switch (currentStage) { - case RenderStage.Before: - case RenderStage.EarlyStatic: - case RenderStage.Static: - return 'Prerender' - case RenderStage.EarlyRuntime: - return 'Prefetch' - case RenderStage.Runtime: - return 'Prefetchable' - case RenderStage.Dynamic: - case RenderStage.Abandoned: - return 'Server' - default: - currentStage satisfies never - throw new InvariantError(`Invalid render stage: ${currentStage}`) - } + return getEnvironmentNameForStage(currentStage) } //=============================================== @@ -4685,9 +4708,21 @@ async function renderWithRestartOnCacheMissInDevWeb( // where sync IO does not cause aborts, so it's okay if it happens before render. const initialRscPayload = await getPayload(requestStore) + const advanceStageIfNoCacheMiss = ( + stage: Parameters[0] + ) => { + if (initialAbandonController.signal.aborted === true) { + return + } else if (cacheSignal.hasPendingReads()) { + initialAbandonController.abort() + } else { + initialStageController.advanceStage(stage) + } + } + const initialStreamResult = await runInSequentialTasks( () => { - initialStageController.advanceStage(RenderStage.EarlyStatic) + initialStageController.advanceStage(RenderStage.ShellEarlyStatic) startTime = performance.now() + performance.timeOrigin const streamPair = teeStream( @@ -4746,40 +4781,28 @@ async function renderWithRestartOnCacheMissInDevWeb( } }, () => { - if (initialAbandonController.signal.aborted === true) { - return - } else if (cacheSignal.hasPendingReads()) { - initialAbandonController.abort() - } else { - initialStageController.advanceStage(RenderStage.Static) - } + advanceStageIfNoCacheMiss(RenderStage.ShellStatic) }, () => { - if (initialAbandonController.signal.aborted === true) { - return - } else if (cacheSignal.hasPendingReads()) { - initialAbandonController.abort() - } else { - initialStageController.advanceStage(RenderStage.EarlyRuntime) - } + advanceStageIfNoCacheMiss(RenderStage.EarlyStatic) }, () => { - if (initialAbandonController.signal.aborted === true) { - return - } else if (cacheSignal.hasPendingReads()) { - initialAbandonController.abort() - } else { - initialStageController.advanceStage(RenderStage.Runtime) - } + advanceStageIfNoCacheMiss(RenderStage.Static) }, () => { - if (initialAbandonController.signal.aborted === true) { - return - } else if (cacheSignal.hasPendingReads()) { - initialAbandonController.abort() - } else { - initialStageController.advanceStage(RenderStage.Dynamic) - } + advanceStageIfNoCacheMiss(RenderStage.ShellEarlyRuntime) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.ShellRuntime) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.EarlyRuntime) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.Runtime) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.Dynamic) } ) @@ -4853,7 +4876,7 @@ async function renderWithRestartOnCacheMissInDevWeb( const finalStreamResult = await runInSequentialTasks( () => { - finalStageController.advanceStage(RenderStage.EarlyStatic) + finalStageController.advanceStage(RenderStage.ShellEarlyStatic) startTime = performance.now() + performance.timeOrigin const streamPair = teeStream( @@ -4883,19 +4906,27 @@ async function renderWithRestartOnCacheMissInDevWeb( } }, () => { - // Static stage + finalStageController.advanceStage(RenderStage.ShellStatic) + }, + () => { + finalStageController.advanceStage(RenderStage.EarlyStatic) + }, + () => { finalStageController.advanceStage(RenderStage.Static) }, () => { - // EarlyRuntime stage + finalStageController.advanceStage(RenderStage.ShellEarlyRuntime) + }, + () => { + finalStageController.advanceStage(RenderStage.ShellRuntime) + }, + () => { finalStageController.advanceStage(RenderStage.EarlyRuntime) }, () => { - // Runtime stage finalStageController.advanceStage(RenderStage.Runtime) }, () => { - // Dynamic stage finalStageController.advanceStage(RenderStage.Dynamic) } ) @@ -4916,6 +4947,29 @@ async function renderWithRestartOnCacheMissInDevWeb( } } +function getEnvironmentNameForStage(stage: RenderStage) { + switch (stage) { + case RenderStage.Before: + case RenderStage.ShellEarlyStatic: + case RenderStage.ShellStatic: + case RenderStage.EarlyStatic: + case RenderStage.Static: + return 'Prerender' + case RenderStage.ShellEarlyRuntime: + case RenderStage.EarlyRuntime: + return 'Prefetch' + case RenderStage.ShellRuntime: + case RenderStage.Runtime: + return 'Prefetchable' + case RenderStage.Dynamic: + case RenderStage.Abandoned: + return 'Server' + default: + stage satisfies never + throw new InvariantError(`Invalid render stage: ${stage}`) + } +} + async function renderWithRestartOnCacheMissInDevNode( ctx: AppRenderContext, initialRequestStore: RequestStore, @@ -4934,22 +4988,7 @@ async function renderWithRestartOnCacheMissInDevNode( const environmentName = () => { const currentStage = requestStore.stagedRendering!.currentStage - switch (currentStage) { - case RenderStage.Before: - case RenderStage.EarlyStatic: - case RenderStage.Static: - return 'Prerender' - case RenderStage.EarlyRuntime: - return 'Prefetch' - case RenderStage.Runtime: - return 'Prefetchable' - case RenderStage.Dynamic: - case RenderStage.Abandoned: - return 'Server' - default: - currentStage satisfies never - throw new InvariantError(`Invalid render stage: ${currentStage}`) - } + return getEnvironmentNameForStage(currentStage) } //=============================================== @@ -4999,9 +5038,21 @@ async function renderWithRestartOnCacheMissInDevNode( // where sync IO does not cause aborts, so it's okay if it happens before render. const initialRscPayload = await getPayload(requestStore) + const advanceStageIfNoCacheMiss = ( + stage: Parameters[0] + ) => { + if (initialAbandonController.signal.aborted === true) { + return + } else if (cacheSignal.hasPendingReads()) { + initialAbandonController.abort() + } else { + initialStageController.advanceStage(stage) + } + } + const initialStreamResult = await runInSequentialTasks( () => { - initialStageController.advanceStage(RenderStage.EarlyStatic) + initialStageController.advanceStage(RenderStage.ShellEarlyStatic) startTime = performance.now() + performance.timeOrigin const sourceStream = workUnitAsyncStorage.run( @@ -5055,40 +5106,28 @@ async function renderWithRestartOnCacheMissInDevNode( } }, () => { - if (initialAbandonController.signal.aborted === true) { - return - } else if (cacheSignal.hasPendingReads()) { - initialAbandonController.abort() - } else { - initialStageController.advanceStage(RenderStage.Static) - } + advanceStageIfNoCacheMiss(RenderStage.ShellStatic) }, () => { - if (initialAbandonController.signal.aborted === true) { - return - } else if (cacheSignal.hasPendingReads()) { - initialAbandonController.abort() - } else { - initialStageController.advanceStage(RenderStage.EarlyRuntime) - } + advanceStageIfNoCacheMiss(RenderStage.EarlyStatic) }, () => { - if (initialAbandonController.signal.aborted === true) { - return - } else if (cacheSignal.hasPendingReads()) { - initialAbandonController.abort() - } else { - initialStageController.advanceStage(RenderStage.Runtime) - } + advanceStageIfNoCacheMiss(RenderStage.Static) }, () => { - if (initialAbandonController.signal.aborted === true) { - return - } else if (cacheSignal.hasPendingReads()) { - initialAbandonController.abort() - } else { - initialStageController.advanceStage(RenderStage.Dynamic) - } + advanceStageIfNoCacheMiss(RenderStage.ShellEarlyRuntime) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.ShellRuntime) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.EarlyRuntime) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.Runtime) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.Dynamic) } ) @@ -5162,7 +5201,7 @@ async function renderWithRestartOnCacheMissInDevNode( const finalStreamResult = await runInSequentialTasks( () => { - finalStageController.advanceStage(RenderStage.EarlyStatic) + finalStageController.advanceStage(RenderStage.ShellEarlyStatic) startTime = performance.now() + performance.timeOrigin const finalSourceStream = workUnitAsyncStorage.run( @@ -5191,19 +5230,27 @@ async function renderWithRestartOnCacheMissInDevNode( } }, () => { - // Static stage + finalStageController.advanceStage(RenderStage.ShellStatic) + }, + () => { + finalStageController.advanceStage(RenderStage.EarlyStatic) + }, + () => { finalStageController.advanceStage(RenderStage.Static) }, () => { - // EarlyRuntime stage + finalStageController.advanceStage(RenderStage.ShellEarlyRuntime) + }, + () => { + finalStageController.advanceStage(RenderStage.ShellRuntime) + }, + () => { finalStageController.advanceStage(RenderStage.EarlyRuntime) }, () => { - // Runtime stage finalStageController.advanceStage(RenderStage.Runtime) }, () => { - // Dynamic stage finalStageController.advanceStage(RenderStage.Dynamic) } ) @@ -5225,20 +5272,29 @@ async function renderWithRestartOnCacheMissInDevNode( } interface AccumulatedStreamChunks { + readonly shellStaticChunks: Array readonly staticChunks: Array + readonly shellRuntimeChunks: Array readonly runtimeChunks: Array readonly dynamicChunks: Array } +function createStageChunksAccumulator(): AccumulatedStreamChunks { + return { + shellStaticChunks: [], + staticChunks: [], + shellRuntimeChunks: [], + runtimeChunks: [], + dynamicChunks: [], + } +} + async function accumulateStreamChunks( stream: AnyStream, stageController: StagedRenderingController, signal: AbortSignal | null ): Promise { - const staticChunks: Array = [] - const runtimeChunks: Array = [] - const dynamicChunks: Array = [] - + const accumulator = createStageChunksAccumulator() if (stream instanceof ReadableStream) { const reader = stream.getReader() @@ -5261,13 +5317,7 @@ async function accumulateStreamChunks( cancel() break } - accumulateChunk( - stageController, - staticChunks, - runtimeChunks, - dynamicChunks, - value - ) + accumulateChunk(stageController.currentStage, accumulator, value) } } catch (err) { // When we cancel the reader we may reject the read. @@ -5294,13 +5344,7 @@ async function accumulateStreamChunks( try { for await (const value of nodeStream) { if (cancelled) break - accumulateChunk( - stageController, - staticChunks, - runtimeChunks, - dynamicChunks, - value - ) + accumulateChunk(stageController.currentStage, accumulator, value) } } catch (err) { if (!cancelled) { @@ -5308,35 +5352,40 @@ async function accumulateStreamChunks( } } } - - return { staticChunks, runtimeChunks, dynamicChunks } + return accumulator } function accumulateChunk( - stageController: StagedRenderingController, - staticChunks: Array, - runtimeChunks: Array, - dynamicChunks: Array, + stage: RenderStage, + accumulator: AccumulatedStreamChunks, value: Uint8Array ): void { - switch (stageController.currentStage) { + switch (stage) { case RenderStage.Before: throw new InvariantError('Unexpected stream chunk while in Before stage') + case RenderStage.ShellEarlyStatic: + case RenderStage.ShellStatic: + accumulator.shellStaticChunks.push(value) + // fall through case RenderStage.EarlyStatic: case RenderStage.Static: - staticChunks.push(value) + accumulator.staticChunks.push(value) + // fall through + case RenderStage.ShellEarlyRuntime: + case RenderStage.ShellRuntime: + accumulator.shellRuntimeChunks.push(value) // fall through case RenderStage.EarlyRuntime: case RenderStage.Runtime: - runtimeChunks.push(value) + accumulator.runtimeChunks.push(value) // fall through case RenderStage.Dynamic: - dynamicChunks.push(value) + accumulator.dynamicChunks.push(value) break case RenderStage.Abandoned: break default: - stageController.currentStage satisfies never + stage satisfies never break } } @@ -6537,7 +6586,7 @@ async function renderWithRestartOnCacheMissInValidation( const initialResult = await runInSequentialTasks( () => { - initialStageController.advanceStage(RenderStage.EarlyStatic) + initialStageController.advanceStage(RenderStage.ShellEarlyStatic) startTime = performance.now() + performance.timeOrigin const stream = workUnitAsyncStorage.run( @@ -6571,9 +6620,21 @@ async function renderWithRestartOnCacheMissInValidation( accumulatedChunksPromise.catch(() => {}) return { accumulatedChunksPromise } }, + () => { + advanceStageIfNoCacheMiss(RenderStage.ShellStatic) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.EarlyStatic) + }, () => { advanceStageIfNoCacheMiss(RenderStage.Static) }, + () => { + advanceStageIfNoCacheMiss(RenderStage.ShellEarlyRuntime) + }, + () => { + advanceStageIfNoCacheMiss(RenderStage.ShellRuntime) + }, () => { advanceStageIfNoCacheMiss(RenderStage.EarlyRuntime) }, @@ -6638,7 +6699,7 @@ async function renderWithRestartOnCacheMissInValidation( const finalResult = await runInSequentialTasks( () => { - finalStageController.advanceStage(RenderStage.EarlyStatic) + finalStageController.advanceStage(RenderStage.ShellEarlyStatic) startTime = performance.now() + performance.timeOrigin const stream = workUnitAsyncStorage.run( @@ -6674,9 +6735,21 @@ async function renderWithRestartOnCacheMissInValidation( accumulatedChunksPromise, } }, + () => { + finalStageController.advanceStage(RenderStage.ShellStatic) + }, + () => { + finalStageController.advanceStage(RenderStage.EarlyStatic) + }, () => { finalStageController.advanceStage(RenderStage.Static) }, + () => { + finalStageController.advanceStage(RenderStage.ShellEarlyRuntime) + }, + () => { + finalStageController.advanceStage(RenderStage.ShellRuntime) + }, () => { finalStageController.advanceStage(RenderStage.EarlyRuntime) }, diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx index 5fba3179fd02..200f918b4eb6 100644 --- a/packages/next/src/server/app-render/create-component-tree.tsx +++ b/packages/next/src/server/app-render/create-component-tree.tsx @@ -42,7 +42,10 @@ import { isNextjsBuiltinFilePath, } from './segment-explorer-path' import type { AppSegmentConfig } from '../../build/segment-config/app/app-segment-config' -import { RenderStage, type StagedRenderingController } from './staged-rendering' +import { + FIRST_LATE_RENDER_STAGE, + type StagedRenderingController, +} from './staged-rendering' /** * Use the provided loader tree to create the React Component tree. @@ -1305,11 +1308,12 @@ function createSeedData( ): CacheNodeSeedData { const createElement = ctx.componentMod.createElement - // When this segment is NOT runtime-prefetchable, delay it until the Static - // stage by wrapping the node in a promise. This allows runtime-prefetchable - // segments (the lower tree) to render first during EarlyStatic, so their - // runtime data resolves in EarlyRuntime where sync IO can be checked. - // React will suspend on the thenable and resume when the stage advances. + // When this segment is NOT runtime-prefetchable, delay it until the ShellStatic + // stage (i.e. the first late stage) by wrapping the node in a promise. + // This allows runtime-prefetchable segments (the lower tree) to render first + // during ShellEarlyStatic, so their runtime data resolves in ShellEarlyRuntime + // where sync IO can be checked. React will suspend on the thenable and resume + // when the stage advances. if (!isRuntimePrefetchable) { const workUnitStore = workUnitAsyncStorage.getStore() if (workUnitStore) { @@ -1321,7 +1325,7 @@ function createSeedData( if (stagedRendering) { const deferredRsc = rsc rsc = stagedRendering - .waitForStage(RenderStage.Static) + .waitForStage(FIRST_LATE_RENDER_STAGE) .then(() => deferredRsc) } break diff --git a/packages/next/src/server/app-render/staged-rendering.ts b/packages/next/src/server/app-render/staged-rendering.ts index a6b600ac7496..d3f24d3052c3 100644 --- a/packages/next/src/server/app-render/staged-rendering.ts +++ b/packages/next/src/server/app-render/staged-rendering.ts @@ -4,14 +4,19 @@ import { createPromiseWithResolvers } from '../../shared/lib/promise-with-resolv export enum RenderStage { Before = 1, // - EarlyStatic = 2, - Static = 3, + ShellEarlyStatic = 10, + ShellStatic = 11, + EarlyStatic = 12, + Static = 13, // - EarlyRuntime = 4, - Runtime = 5, + ShellEarlyRuntime = 20, + ShellRuntime = 21, + EarlyRuntime = 22, + Runtime = 23, // - Dynamic = 6, - Abandoned = 7, + Dynamic = 30, + // + Abandoned = 40, } export type AdvanceableRenderStage = Exclude< @@ -20,15 +25,21 @@ export type AdvanceableRenderStage = Exclude< > export const RENDER_STAGE_ADVANCE_ORDER: AdvanceableRenderStage[] = [ + RenderStage.ShellEarlyStatic, + RenderStage.ShellStatic, RenderStage.EarlyStatic, RenderStage.Static, // + RenderStage.ShellEarlyRuntime, + RenderStage.ShellRuntime, RenderStage.EarlyRuntime, RenderStage.Runtime, // RenderStage.Dynamic, ] +export const FIRST_LATE_RENDER_STAGE = RenderStage.ShellStatic + export function getNextStage( stage: Exclude ) { @@ -37,16 +48,28 @@ export function getNextStage( ] } +export function isAdvanceableRenderStage( + stage: RenderStage +): stage is AdvanceableRenderStage { + return RenderStage.Before < stage && stage <= RenderStage.Dynamic +} + export function isEarlyRenderStage( stage: Exclude ): boolean { switch (stage) { + case RenderStage.ShellEarlyStatic: case RenderStage.EarlyStatic: + case RenderStage.ShellEarlyRuntime: case RenderStage.EarlyRuntime: { return true } + case RenderStage.ShellStatic: case RenderStage.Static: - case RenderStage.Runtime: + case RenderStage.ShellRuntime: + case RenderStage.Runtime: { + return false + } case RenderStage.Dynamic: case RenderStage.Abandoned: { return false @@ -69,9 +92,13 @@ export class StagedRenderingController { syncInterruptReason: Error | null = null triggers: Record = { + [RenderStage.ShellEarlyStatic]: createStageTrigger(), + [RenderStage.ShellStatic]: createStageTrigger(), [RenderStage.EarlyStatic]: createStageTrigger(), [RenderStage.Static]: createStageTrigger(), // + [RenderStage.ShellEarlyRuntime]: createStageTrigger(), + [RenderStage.ShellRuntime]: createStageTrigger(), [RenderStage.EarlyRuntime]: createStageTrigger(), [RenderStage.Runtime]: createStageTrigger(), // @@ -133,13 +160,17 @@ export class StagedRenderingController { case RenderStage.Before: // If we haven't started the render yet, it can't be interrupted. return false + case RenderStage.ShellEarlyStatic: + case RenderStage.ShellStatic: case RenderStage.EarlyStatic: case RenderStage.Static: return true + case RenderStage.ShellEarlyRuntime: case RenderStage.EarlyRuntime: // EarlyRuntime is for runtime-prefetchable segments. Sync IO // should error because it would abort a runtime prefetch. return true + case RenderStage.ShellRuntime: case RenderStage.Runtime: // Runtime is for non-prefetchable segments. Sync IO is fine there // because in practice this segment will never be runtime prefetched @@ -184,8 +215,11 @@ export class StagedRenderingController { // we need to advance to the Dynamic stage and capture the interruption reason. // (in dev, this will be the restarted render) switch (this.currentStage) { + case RenderStage.ShellEarlyStatic: + case RenderStage.ShellStatic: case RenderStage.EarlyStatic: case RenderStage.Static: + case RenderStage.ShellEarlyRuntime: case RenderStage.EarlyRuntime: { // EarlyRuntime is for runtime-prefetchable segments. Sync IO here // means the prefetch would be aborted too early. @@ -193,14 +227,16 @@ export class StagedRenderingController { this.advanceStage(RenderStage.Dynamic) return } + case RenderStage.ShellRuntime: case RenderStage.Runtime: { - // `shouldTrackSyncInterrupt` returns false for Runtime, so we should + // `shouldTrackSyncInterrupt` returns false for [Shell]Runtime, so we should // never get here. Defensive no-op. break } case RenderStage.Dynamic: { // `shouldTrackSyncInterrupt` returns false for Dynamic, so we should // never get here. Defensive no-op. + break } default: { @@ -245,9 +281,13 @@ export class StagedRenderingController { "A render that hasn't started yet cannot be abandoned" ) } + case RenderStage.ShellEarlyStatic: case RenderStage.EarlyStatic: + case RenderStage.ShellStatic: case RenderStage.Static: + case RenderStage.ShellEarlyRuntime: case RenderStage.EarlyRuntime: + case RenderStage.ShellRuntime: case RenderStage.Runtime: { // Resolve all stages after the current one, up to runtime (excluding dynamic) const nextStageIx = RENDER_STAGE_ADVANCE_ORDER.indexOf(currentStage) + 1 @@ -288,9 +328,13 @@ export class StagedRenderingController { switch (currentStage) { case RenderStage.Before: + case RenderStage.ShellEarlyStatic: case RenderStage.EarlyStatic: + case RenderStage.ShellStatic: case RenderStage.Static: + case RenderStage.ShellEarlyRuntime: case RenderStage.EarlyRuntime: + case RenderStage.ShellRuntime: case RenderStage.Runtime: { // Resolve all stages between the current stage and the target. const nextStageIx = From 83c375edb1e6d3932b21c08cc69e3d7d9cc1f872 Mon Sep 17 00:00:00 2001 From: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:11:19 +0200 Subject: [PATCH 2/8] instant: prompts on all fix cards, [group]-tagged CLI bullets, new docs slugs (#94017) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### What? Adds "Copy prompt" button to all 33 instant-guidance fix cards. Updates card links, factory `Learn more:` URLs, and overlay routing to the new docs slugs. Adds `[group]` tag prefix to CLI fix bullets so agents can map them back to card prompts. ### Why? Cards tell developers _what_ to do. The button gives agents a ready-to-paste instruction. The `[group]` tag lets agents reading CLI output find the matching card in the docs without parsing prose. ### How? - `prompt` field on all 33 `FixCard` entries. - Button replaces the external-link icon in the top-right; link moves next to the label. - Card links updated to `blocking-prerender-*` and `instant-unrendered-segment` slugs (avoids overriding upstream pages). - Variant-aware URL routing for `metadata` and `viewport` (matches existing `blocking-route` pattern). `InstantHeaderExplanation` takes a `variant` prop. - Fix bullets prefixed with their card group: `[cache]`, `[stream]`, `[block]`, etc. Tags match `` in the MDX docs. - CLI bullets use `unstable_instant = false` (the current API). Overlay cards keep `instant` (aspirational). - Metadata dynamic-marker bullet now mentions the Suspense wrapper. - Merged canary: unrendered-segment errors land in the Insights tab via `isInstantNavigationError`. ### Depends on - [vercel/front#71640](https://github.com/vercel/front/pull/71640) — 6 sync-IO pages - [vercel/front#71781](https://github.com/vercel/front/pull/71781) — 4 metadata/viewport pages + `instant-unrendered-segment` --- packages/next/.storybook/fixtures/errors.ts | 28 +- packages/next/errors.json | 19 +- .../instant/instant-guidance-data.ts | 201 +- .../components/instant/instant-guidance.tsx | 134 +- .../dev-overlay/container/errors.test.ts | 6 +- .../dev-overlay/container/errors.tsx | 41 +- .../app-render/blocking-route-messages.ts | 101 +- .../server/app-render/dynamic-rendering.ts | 7 +- .../src/server/app-render/sync-io-messages.ts | 47 +- .../cache-components-dev-errors.test.ts | 8 +- ...components-dev-fallback-validation.test.ts | 36 +- .../cache-components-dynamic-imports.test.ts | 3 +- .../cache-components-errors.test.ts | 1718 +++++++++-------- .../instant-validation-build.test.ts | 11 +- .../instant-validation-causes.test.ts | 8 +- .../instant-validation-level-default.test.ts | 2 +- .../instant-validation-level-error.test.ts | 54 +- ...tant-validation-level-manual-error.test.ts | 28 +- ...nt-validation-level-manual-warning.test.ts | 81 +- .../instant-validation-level-warning.test.ts | 21 +- .../instant-validation-static-shells.test.ts | 2 +- .../instant-validation-parallel-slots.test.ts | 93 +- .../instant-validation.test.ts | 588 +++--- ...ata-static-file-intercepting-route.test.ts | 10 +- ...etadata-static-file-parallel-route.test.ts | 10 +- .../metadata-static-file-root-route.test.ts | 10 +- .../metadata-static-file-static-route.test.ts | 10 +- .../missing-suspense-with-csr-bailout.test.ts | 2 +- .../build-output-prerender.test.ts | 66 +- .../src/analyzer/well_known/kinds.rs | 40 +- 30 files changed, 1952 insertions(+), 1433 deletions(-) diff --git a/packages/next/.storybook/fixtures/errors.ts b/packages/next/.storybook/fixtures/errors.ts index dadeca6656cb..04e6b98bea9d 100644 --- a/packages/next/.storybook/fixtures/errors.ts +++ b/packages/next/.storybook/fixtures/errors.ts @@ -360,7 +360,7 @@ export const instantRuntimeDataErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/01-cookies-body": Next.js encountered runtime data during the initial render.\n\n`cookies()`, `headers()`, `params`, or `searchParams` accessed outside of `` prevents the route from being prerendered, blocking navigation and leading to a slower user experience.\n\nWays to fix this:\n - Provide a placeholder with `` around the data access\n - Use `generateStaticParams` to make route params static\n - Set `export const instant = false` to allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-route' + 'Route "/01-cookies-body": Next.js encountered runtime data during prerendering.\n\n`cookies()`, `headers()`, `params`, or `searchParams` accessed outside of `` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.\n\nWays to fix this:\n - [stream] Provide a placeholder with `` around the data access\n - [cache] If the runtime data is `params` and they\'re known, prerender them with `generateStaticParams`\n - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-route' ), { __NEXT_ERROR_CODE: 'E1221' } ), @@ -388,7 +388,7 @@ export const instantUncachedDataErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/06-uncached-fetch-body": Next.js encountered uncached data during the initial render.\n\n`fetch(...)` or `connection()` accessed outside of `` prevents the route from being prerendered, blocking navigation and leading to a slower user experience.\n\nWays to fix this:\n - Cache the data access with `"use cache"`\n - Provide a placeholder with `` around the data access\n - Set `export const instant = false` to allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-route' + 'Route "/06-uncached-fetch-body": Next.js encountered uncached data during prerendering.\n\n`fetch(...)` or `connection()` accessed outside of `` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.\n\nWays to fix this:\n - [cache] Cache the data access with `"use cache"`\n - [stream] Provide a placeholder with `` around the data access\n - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-route' ), { __NEXT_ERROR_CODE: 'E1220' } ), @@ -416,7 +416,7 @@ export const instantViewportErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/14-cookies-in-viewport": Next.js encountered runtime data in `generateViewport()`.\n\n`cookies()`, `headers()`, `params`, or `searchParams` in `generateViewport()` prevents the page from being prerendered, leading to a slower user experience.\n\nWays to fix this:\n - Use a static viewport export instead of `generateViewport()`\n - Wrap your document `` in ``\n - Set `export const instant = false` to allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport' + 'Route "/14-cookies-in-viewport": Next.js encountered runtime data in `generateViewport()`.\n\n`cookies()`, `headers()`, `params`, or `searchParams` in `generateViewport()` prevents the page from being prerendered, leading to a slower user experience.\n\nWays to fix this:\n - [static] Use a static viewport export instead of `generateViewport()`\n - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-prerender-viewport-runtime' ), { __NEXT_ERROR_CODE: 'E1208' } ), @@ -447,7 +447,7 @@ export const instantViewportUncachedErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/15-fetch-in-viewport": Next.js encountered uncached data in `generateViewport()`.\n\n`fetch(...)` or `connection()` in `generateViewport()` prevents the page from being prerendered, leading to a slower user experience.\n\nWays to fix this:\n - Cache the viewport data with `"use cache"` in `generateViewport()`\n - Wrap your document `` in ``\n - Set `export const instant = false` to allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/next-prerender-dynamic-viewport' + 'Route "/15-fetch-in-viewport": Next.js encountered uncached data in `generateViewport()`.\n\n`fetch(...)` or `connection()` in `generateViewport()` prevents the page from being prerendered, leading to a slower user experience.\n\nWays to fix this:\n - [cache] Cache the viewport data with `"use cache"` in `generateViewport()`\n - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic' ), { __NEXT_ERROR_CODE: 'E1210' } ), @@ -477,7 +477,7 @@ export const instantMetadataErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/12-cookies-in-metadata": Next.js encountered runtime data in `generateMetadata()`.\n\nThis route\'s metadata is blocked, but the rest of its content can be prerendered. `cookies()`, `headers()`, `params`, or `searchParams` accessed in `generateMetadata()` cause it to run dynamically.\n\nWays to fix this:\n - Use a static metadata export instead of `generateMetadata()`\n - Add a dynamic data access (e.g. `await connection()`) to the page to render it at request time\n\nLearn more: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata' + 'Route "/12-cookies-in-metadata": Next.js encountered runtime data in `generateMetadata()`.\n\nThis route\'s metadata is blocked, but the rest of its content can be prerendered. `cookies()`, `headers()`, `params`, or `searchParams` accessed in `generateMetadata()` cause it to run dynamically.\n\nWays to fix this:\n - [static] Use a static metadata export instead of `generateMetadata()`\n - [dynamic] Render a marker component that calls `await connection()` inside `` on the page\n\nLearn more: https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime' ), { __NEXT_ERROR_CODE: 'E1230' } ), @@ -508,9 +508,9 @@ export const instantMetadataUncachedErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/13-fetch-in-metadata": Next.js encountered uncached data in `generateMetadata()`.\n\nThis route\'s metadata is blocked, but the rest of its content can be prerendered. `fetch(...)` or `connection()` accessed in `generateMetadata()` cause it to run dynamically.\n\nWays to fix this:\n - Cache the metadata with `"use cache"` in `generateMetadata()`\n - Add a dynamic data access (e.g. `await connection()`) to the page to render it at request time\n\nLearn more: https://nextjs.org/docs/messages/next-prerender-dynamic-metadata' + 'Route "/13-fetch-in-metadata": Next.js encountered uncached data in `generateMetadata()`.\n\nThis route\'s metadata is blocked, but the rest of its content can be prerendered. `fetch(...)` or `connection()` accessed in `generateMetadata()` cause it to run dynamically.\n\nWays to fix this:\n - [cache] Cache the metadata with `"use cache"` in `generateMetadata()`\n - [dynamic] Render a marker component that calls `await connection()` inside `` on the page\n\nLearn more: https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic' ), - { __NEXT_ERROR_CODE: 'E1231' } + { __NEXT_ERROR_CODE: 'E1308' } ), frames: createStoryFrames({ reason: @@ -539,7 +539,7 @@ export const instantCurrentTimeErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/39-date-now-no-instant": Next.js encountered `Date.now()` without an explicit rendering intent.\n\nThis value can change between renders, so it must be either prerendered or computed later.\n\nWays to fix this:\n - Render at request time by adding a dynamic data access (e.g. `await connection()`) before this call\n - Prerender and cache the value with `"use cache"`\n - Render the value on the client with `"use client"`\n - Measure elapsed time with `performance.now()` instead of `Date.now()`\n\nLearn more: https://nextjs.org/docs/messages/next-prerender-current-time' + 'Route "/39-date-now-no-instant": Next.js encountered `Date.now()` without an explicit rendering intent.\n\nThis value can change between renders, so it must be either prerendered or computed later.\n\nWays to fix this:\n - [dynamic] Render at request time by adding a dynamic data access (e.g. `await connection()`) before this call\n - [cache] Prerender and cache the value with `"use cache"`\n - [client] Render the value on the client with `"use client"`\n - Measure elapsed time with `performance.now()` instead of `Date.now()`\n\nLearn more: https://nextjs.org/docs/messages/blocking-prerender-current-time' ), { __NEXT_ERROR_CODE: 'E1247' } ), @@ -568,7 +568,7 @@ export const instantMathRandomErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/38-math-random-no-instant": Next.js encountered `Math.random()` without an explicit rendering intent.\n\nThis value can change between renders, so it must be either prerendered or computed later.\n\nWays to fix this:\n - Render at request time by adding a dynamic data access (e.g. `await connection()`) before this call\n - Prerender and cache the value with `"use cache"`\n - Render the value on the client with `"use client"`\n\nLearn more: https://nextjs.org/docs/messages/next-prerender-random' + 'Route "/38-math-random-no-instant": Next.js encountered `Math.random()` without an explicit rendering intent.\n\nThis value can change between renders, so it must be either prerendered or computed later.\n\nWays to fix this:\n - [dynamic] Render at request time by adding a dynamic data access (e.g. `await connection()`) before this call\n - [cache] Prerender and cache the value with `"use cache"`\n - [client] Render the value on the client with `"use client"`\n\nLearn more: https://nextjs.org/docs/messages/blocking-prerender-random' ), { __NEXT_ERROR_CODE: 'E1247' } ), @@ -597,7 +597,7 @@ export const instantCryptoRandomUUIDErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/40-crypto-random-no-instant": Next.js encountered `crypto.randomUUID()` without an explicit rendering intent.\n\nThis value can change between renders, so it must be either prerendered or computed later.\n\nWays to fix this:\n - Render at request time by adding a dynamic data access (e.g. `await connection()`) before this call\n - Prerender and cache the value with `"use cache"`\n - Render the value on the client with `"use client"`\n\nLearn more: https://nextjs.org/docs/messages/next-prerender-crypto' + 'Route "/40-crypto-random-no-instant": Next.js encountered `crypto.randomUUID()` without an explicit rendering intent.\n\nThis value can change between renders, so it must be either prerendered or computed later.\n\nWays to fix this:\n - [dynamic] Render at request time by adding a dynamic data access (e.g. `await connection()`) before this call\n - [cache] Prerender and cache the value with `"use cache"`\n - [client] Render the value on the client with `"use client"`\n\nLearn more: https://nextjs.org/docs/messages/blocking-prerender-crypto' ), { __NEXT_ERROR_CODE: 'E1247' } ), @@ -626,7 +626,7 @@ export const instantClientMathRandomErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/44-client-math-random-no-suspense": Next.js encountered `Math.random()` in a Client Component.\n\nThis value would be evaluated during the prerender and fixed at build time, instead of recomputed on each visit.\n\nWays to fix this:\n - Wrap the Client Component in ``\n - Move the read into a `useEffect` or event handler\n\nLearn more: https://nextjs.org/docs/messages/next-prerender-random-client' + 'Route "/44-client-math-random-no-suspense": Next.js encountered `Math.random()` in a Client Component.\n\nThis value would be evaluated during the prerender and fixed at build time, instead of recomputed on each visit.\n\nWays to fix this:\n - [stream] Wrap the Client Component in ``\n - [defer] Move the read into a `useEffect` or event handler\n\nLearn more: https://nextjs.org/docs/messages/blocking-prerender-random-client' ), { __NEXT_ERROR_CODE: 'E1228' } ), @@ -655,7 +655,7 @@ export const instantUnrenderedSegmentErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/81-instant-wrapper-unrendered-segment/trigger": Could not validate that a segment in your UI has instant navigation.\n\nThis segment was dropped from rendering. Issues that would prevent instant navigation will go undetected.\n\nDropped segment:\n test-app/app/81-instant-wrapper-unrendered-segment/trigger/page.tsx\n\nWays to fix this:\n - Render the dropped segment\n - Set `export const instant = false` on the dropped segment to skip validation\n\nLearn more: https://nextjs.org/docs/messages/unrendered-instant-segment' + 'Route "/81-instant-wrapper-unrendered-segment/trigger": Could not validate that a segment in your UI has instant navigation.\n\nThis segment was dropped from rendering. Issues that would prevent instant navigation will go undetected.\n\nDropped segment:\n test-app/app/81-instant-wrapper-unrendered-segment/trigger/page.tsx\n\nWays to fix this:\n - [render] Render the dropped segment\n - [ignore] Set `export const unstable_instant = false` on the dropped segment to skip validation\n\nLearn more: https://nextjs.org/docs/messages/instant-unrendered-segment' ), { __NEXT_ERROR_CODE: 'E1286' } ), @@ -672,7 +672,7 @@ export const mixedIssueAndInsightErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/nav-cookies-under-suspense": Next.js encountered runtime data during the initial render or a navigation.\n\n`cookies()`, `headers()`, `params`, or `searchParams` accessed under `` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\n\nWays to fix this:\n - Use `generateStaticParams` to make route params static\n - Provide a placeholder with `` around the data access\n - Set `export const instant = false` to allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-route' + 'Route "/nav-cookies-under-suspense": Next.js encountered runtime data during prerendering or a navigation.\n\n`cookies()`, `headers()`, `params`, or `searchParams` accessed outside of `` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\n\nWays to fix this:\n - [stream] Provide a placeholder with `` around the data access\n - [cache] If the runtime data is `params` and they\'re known, prerender them with `generateStaticParams`\n - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-route' ), { __NEXT_ERROR_CODE: 'E1247' } ), @@ -697,7 +697,7 @@ export const mixedIssueAndInsightErrors: ReadyRuntimeError[] = [ runtime: true, error: Object.assign( new Error( - 'Route "/nav-fetch-under-suspense": Next.js encountered uncached data during the initial render or a navigation.\n\n`fetch(...)` or `connection()` accessed under `` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\n\nWays to fix this:\n - Cache the data access with `"use cache"`\n - Provide a placeholder with `` around the data access\n - Set `export const instant = false` to allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-route' + 'Route "/nav-fetch-under-suspense": Next.js encountered uncached data during prerendering or a navigation.\n\n`fetch(...)` or `connection()` accessed outside of `` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\n\nWays to fix this:\n - [cache] Cache the data access with `"use cache"`\n - [stream] Provide a placeholder with `` around the data access\n - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route\n\nLearn more: https://nextjs.org/docs/messages/blocking-route' ), { __NEXT_ERROR_CODE: 'E1246' } ), diff --git a/packages/next/errors.json b/packages/next/errors.json index 7e6b987acf5e..b190b445702e 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -1285,7 +1285,20 @@ "1284": "Cache Components error recovery expected an original prerender store", "1285": "Cache Components error recovery expected an original resume data cache", "1286": "Route \"%s\": Could not validate that a segment in your UI has instant navigation.", - "1287": "A render that hasn't started yet cannot be abandoned", - "1288": "Cannot determine late/early stage before starting the render", - "1289": "Attempted to advance to stage %s but the render is limited to %s" + "1287": "Route \"%s\": Next.js encountered the unstable value %s in a Client Component.\\n\\nThis value would be evaluated during the prerender, instead of recomputed on each visit.\\n\\nWays to fix this:\\n - [stream] Wrap the Client Component in \\`\\`\\n %s#wrap-in-or-move-into-suspense\\n - [defer] Move the read into a \\`useEffect\\` or event handler\\n %s#move-into-effect-or-event-handler%s", + "1288": "Route \"%s\": Next.js encountered runtime data in \\`generateMetadata()\\`.\\n\\nThis route's metadata is blocked, but the rest of its content can be prerendered. \\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` accessed in \\`generateMetadata()\\` cause it to run dynamically.\\n\\nWays to fix this:\\n - [static] Use a static metadata export instead of \\`generateMetadata()\\`\\n https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime#use-static-metadata\\n - [dynamic] Render a marker component that calls \\`await connection()\\` inside \\`\\` on the page\\n https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime#mark-the-route-as-dynamic", + "1289": "Route \"%s\": Next.js encountered uncached data in \\`generateViewport()\\`.\\n\\n\\`fetch(...)\\` or \\`connection()\\` in \\`generateViewport()\\` prevents the page from being prerendered, leading to a slower user experience.\\n\\nWays to fix this:\\n - [cache] Cache the viewport data with \\`\"use cache\"\\` in \\`generateViewport()\\`\\n https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#cache-the-viewport-data\\n - [block] Set \\`export const unstable_instant = false\\` to silence this warning and allow a blocking route\\n https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#allow-blocking-route", + "1290": "Route \"%s\": Next.js encountered uncached data during prerendering.\\n\\n\\`fetch(...)\\` or \\`connection()\\` accessed outside of \\`\\` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.\\n\\nWays to fix this:\\n - [cache] Cache the data access with \\`\"use cache\"\\`\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data\\n - [stream] Provide a placeholder with \\`\\` around the data access\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense\\n - [block] Set \\`export const unstable_instant = false\\` to silence this warning and allow a blocking route\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route", + "1291": "Route \"%s\": Next.js encountered uncached or runtime data in \\`generateViewport()\\`.\\n\\nThis prevents the page from being prerendered, leading to a slower user experience.\\n\\nWays to fix this:\\n - [static] Use a static viewport export instead of \\`generateViewport()\\`\\n https://nextjs.org/docs/messages/blocking-prerender-viewport-runtime#use-static-viewport\\n - [cache] Cache the viewport data with \\`\"use cache\"\\` in \\`generateViewport()\\`\\n https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#cache-the-viewport-data\\n - [block] Set \\`export const unstable_instant = false\\` to silence this warning and allow a blocking route\\n https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#allow-blocking-route", + "1292": "Route \"%s\": Next.js encountered uncached data in \\`generateMetadata()\\`.\\n\\nThis route's metadata is blocked, but the rest of its content can be prerendered. \\`fetch(...)\\` or \\`connection()\\` accessed in \\`generateMetadata()\\` cause it to run dynamically.\\n\\nWays to fix this:\\n - [cache] Cache the metadata with \\`\"use cache\"\\` in \\`generateMetadata()\\`\\n https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#cache-the-metadata\\n - [dynamic] Render a marker component that calls \\`await connection()\\` inside \\`\\` on the page\\n https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#mark-the-route-as-dynamic", + "1293": "Route \"%s\": Next.js encountered runtime data in \\`generateViewport()\\`.\\n\\n\\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` in \\`generateViewport()\\` prevents the page from being prerendered, leading to a slower user experience.\\n\\nWays to fix this:\\n - [static] Use a static viewport export instead of \\`generateViewport()\\`\\n https://nextjs.org/docs/messages/blocking-prerender-viewport-runtime#use-static-viewport\\n - [block] Set \\`export const unstable_instant = false\\` to silence this warning and allow a blocking route\\n https://nextjs.org/docs/messages/blocking-prerender-viewport-runtime#allow-blocking-route", + "1294": "Route \"%s\": Next.js encountered uncached or runtime data in \\`generateMetadata()\\`.\\n\\nThis route's metadata is blocked, but the rest of its content can be prerendered.\\n\\nWays to fix this:\\n - [static] Use a static metadata export instead of \\`generateMetadata()\\`\\n https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime#use-static-metadata\\n - [cache] Cache the metadata with \\`\"use cache\"\\` in \\`generateMetadata()\\`\\n https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#cache-the-metadata\\n - [dynamic] Render a marker component that calls \\`await connection()\\` inside \\`\\` on the page\\n https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#mark-the-route-as-dynamic", + "1295": "Route \"%s\": Next.js encountered the unstable value %s while prerendering.\\n\\nThis value can change between renders, so it must be either prerendered or computed later.\\n\\nWays to fix this:\\n - [dynamic] Render at request time by adding a dynamic data access (e.g. \\`await connection()\\`) before this call\\n %s#generate-on-every-request\\n - [cache] Prerender and cache the value with \\`\"use cache\"\\`\\n %s%s\\n - [client] Render the value on the client with \\`\"use client\"\\`\\n %s#render-on-the-client%s", + "1296": "Route \"%s\": Next.js encountered runtime data during prerendering.\\n\\n\\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` accessed outside of \\`\\` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.\\n\\nWays to fix this:\\n - [stream] Provide a placeholder with \\`\\` around the data access\\n https://nextjs.org/docs/messages/blocking-prerender-runtime#wrap-in-or-move-into-suspense\\n - [cache] If the runtime data is \\`params\\` and they're known, prerender them with \\`generateStaticParams\\`\\n https://nextjs.org/docs/messages/blocking-prerender-runtime#for-known-params-prerender\\n - [block] Set \\`export const unstable_instant = false\\` to silence this warning and allow a blocking route\\n https://nextjs.org/docs/messages/blocking-prerender-runtime#allow-blocking-route", + "1297": "Route \"%s\": Next.js encountered runtime data during prerendering or a navigation.\\n\\n\\`cookies()\\`, \\`headers()\\`, \\`params\\`, or \\`searchParams\\` accessed outside of \\`\\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\\n\\nWays to fix this:\\n - [stream] Provide a placeholder with \\`\\` around the data access\\n https://nextjs.org/docs/messages/blocking-prerender-runtime#wrap-in-or-move-into-suspense\\n - [cache] If the runtime data is \\`params\\` and they're known, prerender them with \\`generateStaticParams\\`\\n https://nextjs.org/docs/messages/blocking-prerender-runtime#for-known-params-prerender\\n - [block] Set \\`export const unstable_instant = false\\` to silence this warning and allow a blocking route\\n https://nextjs.org/docs/messages/blocking-prerender-runtime#allow-blocking-route", + "1298": "Route \"%s\": Next.js encountered uncached data during prerendering or a navigation.\\n\\n\\`fetch(...)\\` or \\`connection()\\` accessed outside of \\`\\` prevents the route from being prerendered or the navigation from being instant, leading to a slower user experience.\\n\\nWays to fix this:\\n - [cache] Cache the data access with \\`\"use cache\"\\`\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data\\n - [stream] Provide a placeholder with \\`\\` around the data access\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense\\n - [block] Set \\`export const unstable_instant = false\\` to silence this warning and allow a blocking route\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route", + "1299": "Route \"%s\": Next.js encountered uncached or runtime data during prerendering.\\n\\n\\`fetch(...)\\`, \\`cookies()\\`, \\`headers()\\`, \\`params\\`, \\`searchParams\\`, or \\`connection()\\` accessed outside of \\`\\` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.\\n\\nWays to fix this:\\n - [cache] Cache the data access with \\`\"use cache\"\\`\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data\\n - [stream] Provide a placeholder with \\`\\` around the data access\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense\\n - [cache] If the runtime data is \\`params\\` and they're known, prerender them with \\`generateStaticParams\\`\\n https://nextjs.org/docs/messages/blocking-prerender-runtime#for-known-params-prerender\\n - [block] Set \\`export const unstable_instant = false\\` to silence this warning and allow a blocking route\\n https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route", + "1300": "A render that hasn't started yet cannot be abandoned", + "1301": "Cannot determine late/early stage before starting the render", + "1302": "Attempted to advance to stage %s but the render is limited to %s" } diff --git a/packages/next/src/next-devtools/dev-overlay/components/instant/instant-guidance-data.ts b/packages/next/src/next-devtools/dev-overlay/components/instant/instant-guidance-data.ts index 0f17e4026f3a..c35801231924 100644 --- a/packages/next/src/next-devtools/dev-overlay/components/instant/instant-guidance-data.ts +++ b/packages/next/src/next-devtools/dev-overlay/components/instant/instant-guidance-data.ts @@ -50,6 +50,11 @@ export type FixCard = { /** Docs URL the card links to, or `null` for no link. */ link: string | null snippets: Snippet[] + /** + * AI-agent prompt copied when the user presses the "Copy prompt" button on + * the card. Phrased as an instruction the agent can act on directly. + */ + prompt?: string } export type SnippetPart = { @@ -73,18 +78,20 @@ const runtimeCards: FixCard[] = [ id: 'wrap-in-or-move-into-suspense', title: 'Wrap in or move into Suspense', group: 'stream', - link: 'https://nextjs.org/docs/messages/blocking-route#wrap-in-or-move-into-suspense', + link: 'https://nextjs.org/docs/messages/blocking-prerender-runtime#wrap-in-or-move-into-suspense', snippets: [ { text: '', highlight: true }, { text: ' ' }, { text: '', highlight: true }, ], + prompt: + 'Wrap the component that reads cookies(), headers(), params, or searchParams in . The fallback prop must render synchronous, deterministic JSX (no fetch, no awaiting, no Math.random or Date.now) that approximates the final layout (skeleton, spinner, or stable placeholder text). Import Suspense from "react". Do not change the data access call. Place the Suspense boundary as close to the access as possible so the cached content above remains in the static shell. If the access is deep in a tree and used for a small piece of UI, prefer to push the access down to the leaf component that needs it instead of awaiting it at the top and forwarding the value.', }, { - id: 'prerender-known-params', + id: 'for-known-params-prerender', title: 'For known params, prerender', group: 'cache', - link: 'https://nextjs.org/docs/messages/blocking-route#prerender-known-params', + link: 'https://nextjs.org/docs/messages/blocking-prerender-runtime#for-known-params-prerender', snippets: [ { text: 'function generateStaticParams() {', @@ -103,16 +110,20 @@ const runtimeCards: FixCard[] = [ }, { text: '}' }, ], + prompt: + 'Add a generateStaticParams() export to the dynamic segment. Return an array of param objects whose keys match the segment\'s [param] names. Each entry is prerendered into static HTML at build time. With Cache Components, requests for params not in the list are served a fallback shell and the route is upgraded in the background. Return a subset of known params for common routes (popular categories, top locales, recent slugs); rare or open-ended params will fall back at runtime. Do not introduce new imports beyond Next.js types. If you can\'t return at least one known param at build time, use "Wrap in or move into Suspense" instead.', }, { id: 'allow-blocking-route', title: 'Allow blocking route', group: 'block', - link: 'https://nextjs.org/docs/messages/blocking-route#allow-blocking-route', + link: 'https://nextjs.org/docs/messages/blocking-prerender-runtime#allow-blocking-route', snippets: [ { text: '// page.tsx or layout.tsx' }, { text: 'export const instant = false', highlight: true }, ], + prompt: + 'Add "export const instant = false" as a top-level export in the page or layout file. This silences the warning for this segment. Confirm with the user that the route is intentionally request-time before applying this change: the export exempts the segment from instant-navigation validation, and the route renders on every request, so navigations to it block until the render completes. If the user wants to keep the navigation instant, choose "Wrap in or move into Suspense" or "Prerender known params" instead.', }, ] @@ -121,33 +132,39 @@ const dynamicCards: FixCard[] = [ id: 'cache-the-component-or-data', title: 'Cache the component or data', group: 'cache', - link: 'https://nextjs.org/docs/messages/blocking-route#cache-the-component-or-data', + link: 'https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data', snippets: [ { text: 'async function Posts() {' }, { text: ' "use cache"', highlight: true }, { text: ' return ' }, ], + prompt: + 'Convert the highlighted data access into a cached function. Put "use cache" as the first statement of the function body. If the value depends on input that changes between calls, accept the input as a function argument so it becomes part of the cache key. Optionally call cacheTag(tag) to allow invalidation via revalidateTag(tag), and cacheLife(profile) to set automatic expiration. Do not move the call site. Do not introduce new imports beyond "next/cache".', }, { id: 'wrap-in-or-move-into-suspense', title: 'Wrap in or move into Suspense', group: 'stream', - link: 'https://nextjs.org/docs/messages/blocking-route#wrap-in-or-move-into-suspense', + link: 'https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense', snippets: [ { text: '', highlight: true }, { text: ' ' }, { text: '', highlight: true }, ], + prompt: + 'Wrap the component that performs the failing data access in . The fallback prop must render synchronous, deterministic JSX (no fetch, no awaiting, no Math.random or Date.now) that approximates the final layout (skeleton, spinner, or stable placeholder text). Import Suspense from "react". Do not change the data fetching logic. If the surrounding parent component already has cached content, place the Suspense boundary as close to the data access as possible so the cached content remains in the static shell.', }, { id: 'allow-blocking-route', title: 'Allow blocking route', group: 'block', - link: 'https://nextjs.org/docs/messages/blocking-route#allow-blocking-route', + link: 'https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route', snippets: [ { text: '// page.tsx or layout.tsx' }, { text: 'export const instant = false', highlight: true }, ], + prompt: + 'Add "export const instant = false" as a top-level export in the page or layout file. This silences the warning for this segment. Confirm with the user that the route is intentionally request-time before applying this change: the export exempts the segment from instant-navigation validation, and the route renders on every request, so navigations to it block until the render completes. If the user wants to keep the navigation instant, choose "Cache the component or data" or "Wrap in or move into Suspense" instead.', }, ] @@ -158,7 +175,7 @@ const unrenderedSegmentCards: FixCard[] = [ id: 'render-the-dropped-segment', title: 'Render the dropped segment', group: 'render', - link: 'https://nextjs.org/docs/messages/unrendered-instant-segment#render-the-dropped-segment', + link: 'https://nextjs.org/docs/messages/instant-unrendered-segment#render-the-dropped-segment', snippets: [ { text: 'function Layout({ children }) {', @@ -178,17 +195,21 @@ const unrenderedSegmentCards: FixCard[] = [ }, { text: '}' }, ], + prompt: + 'Ensure the layout renders {children} so the dropped segment is included in the render tree. If the layout conditionally omits {children} (e.g. showing a login page instead), restructure so both branches render {children} and use a Suspense boundary or conditional content inside the child segment instead. If the segment is a parallel route slot, ensure the layout renders the slot prop.', }, { id: 'skip-validation-on-the-segment', title: 'Skip validation on the segment', group: 'ignore', - link: 'https://nextjs.org/docs/messages/unrendered-instant-segment#skip-validation-on-the-segment', + link: 'https://nextjs.org/docs/messages/instant-unrendered-segment#skip-validation-on-the-segment', snippets: [ { text: '// page.tsx or layout.tsx' }, { text: '' }, { text: 'export const instant = false', highlight: true }, ], + prompt: + 'Add "export const unstable_instant = false" as a top-level export in the dropped segment\'s page or layout file. This silences the warning for the dropped segment and tells Next.js the segment does not need instant-navigation validation. Confirm with the user that skipping validation is intentional before applying this change.', }, ] @@ -199,22 +220,26 @@ const metadataRuntimeCards: FixCard[] = [ id: 'use-static-metadata', title: 'Use static metadata', group: 'static', - link: 'https://nextjs.org/docs/messages/next-prerender-dynamic-metadata#use-static-metadata', + link: 'https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime#use-static-metadata', snippets: [ { text: 'export const metadata = {', highlight: true }, { text: ' title: "My Page"' }, { text: '}' }, ], + prompt: + 'Replace the generateMetadata() function with a static metadata export. Convert all dynamic values to static strings. If the metadata depends on params, use generateStaticParams instead to prerender each variant. Do not introduce new imports.', }, { id: 'render-page-at-request-time', title: 'Mark the route as dynamic', group: 'dynamic', - link: 'https://nextjs.org/docs/messages/next-prerender-dynamic-metadata#render-page-at-request-time', + link: 'https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime#mark-the-route-as-dynamic', snippets: [ { text: '// page.tsx or layout.tsx' }, { text: 'await connection()', highlight: true }, ], + prompt: + 'Add "await connection()" from "next/server" inside a component rendered by the page, wrapped in . The component can render null. This creates a dynamic hole inside Suspense so the rest of the page can still prerender, while signalling to Next.js that the dynamic metadata is intentional. Use this fix when the page would otherwise have no dynamic content other than the metadata.', }, ] @@ -223,22 +248,26 @@ const metadataDynamicCards: FixCard[] = [ id: 'cache-the-metadata', title: 'Cache the metadata', group: 'cache', - link: 'https://nextjs.org/docs/messages/next-prerender-dynamic-metadata#cache-the-metadata', + link: 'https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#cache-the-metadata', snippets: [ { text: 'async function generateMetadata() {' }, { text: ' "use cache"', highlight: true }, { text: ' return await cms.getMeta(…)' }, ], + prompt: + 'Add "use cache" as the first statement inside generateMetadata(). This caches the metadata so it can be included in the prerender. Optionally call cacheTag(tag) so the entry can be invalidated on-demand from a Server Action via updateTag(tag), or from a Route Handler via revalidateTag(tag, "max") for stale-while-revalidate semantics. Optionally call cacheLife(profile) to control how long the cache lives before background revalidation or full expiration. Do not introduce new imports beyond "next/cache".', }, { id: 'render-page-at-request-time', title: 'Mark the route as dynamic', group: 'dynamic', - link: 'https://nextjs.org/docs/messages/next-prerender-dynamic-metadata#render-page-at-request-time', + link: 'https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#mark-the-route-as-dynamic', snippets: [ { text: '// page.tsx or layout.tsx' }, { text: 'await connection()', highlight: true }, ], + prompt: + 'Add "await connection()" from "next/server" inside a component rendered by the page, wrapped in . The component can render null. This creates a dynamic hole inside Suspense so the rest of the page can still prerender, while signalling to Next.js that the dynamic metadata is intentional. Use this fix when the page would otherwise have no dynamic content other than the metadata.', }, ] @@ -249,22 +278,26 @@ const viewportRuntimeCards: FixCard[] = [ id: 'use-static-viewport', title: 'Use static viewport', group: 'static', - link: 'https://nextjs.org/docs/messages/next-prerender-dynamic-viewport#use-static-viewport', + link: 'https://nextjs.org/docs/messages/blocking-prerender-viewport-runtime#use-static-viewport', snippets: [ { text: 'export const viewport = {', highlight: true }, { text: ' themeColor: "#000"' }, { text: '}' }, ], + prompt: + 'Replace the generateViewport() function with a static viewport export. Convert all dynamic values to static ones. Do not introduce new imports.', }, { id: 'allow-blocking-route', title: 'Allow blocking route', group: 'block', - link: 'https://nextjs.org/docs/messages/next-prerender-dynamic-viewport#allow-blocking-route', + link: 'https://nextjs.org/docs/messages/blocking-prerender-viewport-runtime#allow-blocking-route', snippets: [ { text: '// page.tsx or layout.tsx' }, { text: 'export const instant = false', highlight: true }, ], + prompt: + 'Add "export const unstable_instant = false" as a top-level export in the page or layout file. This silences the warning for this segment. Confirm with the user that the route is intentionally fully dynamic before applying this change: the export exempts the segment from instant-navigation validation, and the route renders on every request. If the user wants to keep the navigation instant, choose "Use static viewport" instead.', }, ] @@ -273,22 +306,26 @@ const viewportDynamicCards: FixCard[] = [ id: 'cache-viewport-data', title: 'Cache the viewport data', group: 'cache', - link: 'https://nextjs.org/docs/messages/next-prerender-dynamic-viewport#cache-viewport-data', + link: 'https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#cache-the-viewport-data', snippets: [ { text: 'async function generateViewport() {' }, { text: ' "use cache"', highlight: true }, { text: ' return await db.getViewport(…)' }, ], + prompt: + 'Add "use cache" as the first statement inside generateViewport(). This caches the viewport so Next.js can include it in the prerender. Optionally call cacheLife(profile) to set automatic expiration. Do not introduce new imports beyond "next/cache".', }, { id: 'allow-blocking-route', title: 'Allow blocking route', group: 'block', - link: 'https://nextjs.org/docs/messages/next-prerender-dynamic-viewport#allow-blocking-route', + link: 'https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#allow-blocking-route', snippets: [ { text: '// page.tsx or layout.tsx' }, { text: 'export const instant = false', highlight: true }, ], + prompt: + 'Add "export const unstable_instant = false" as a top-level export in the page or layout file. This silences the warning for this segment. Confirm with the user that the route is intentionally fully dynamic before applying this change: the export exempts the segment from instant-navigation validation, and the route renders on every request. If the user wants to keep the navigation instant, choose "Cache the viewport data" instead.', }, ] @@ -299,34 +336,40 @@ const syncMathCards: FixCard[] = [ id: 'render-at-request-time', title: 'Generate on every request', group: 'dynamic', - link: 'https://nextjs.org/docs/messages/next-prerender-random#render-at-request-time', + link: 'https://nextjs.org/docs/messages/blocking-prerender-random#generate-on-every-request', snippets: [ { text: 'await connection()', highlight: true }, { text: 'const id = Math.random()' }, { text: 'return ' }, ], + prompt: + 'Add "await connection()" from "next/server" immediately before the Math.random() call. This marks the component as request-time, so Next.js excludes it from the prerendered HTML and streams it in from the nearest boundary on each request. Do not change the call site of Math.random() itself. Only change the call site once you\'ve confirmed with the user that a fresh value on every request is the intent.', }, { id: 'cache-the-random-value', title: 'Cache the random value', group: 'cache', - link: 'https://nextjs.org/docs/messages/next-prerender-random#cache-the-random-value', + link: 'https://nextjs.org/docs/messages/blocking-prerender-random#cache-the-random-value', snippets: [ { text: 'function RandomId() {' }, { text: ' "use cache"', highlight: true }, { text: ' return String(Math.random())' }, ], + prompt: + 'Move the Math.random() call into its own function or component and add "use cache" as the first statement of the body. Optionally call cacheLife(profile) to control how long the same random value is reused before regeneration. Do not introduce new imports beyond "next/cache".', }, { id: 'render-on-the-client', title: 'Render on the client', group: 'client', - link: 'https://nextjs.org/docs/messages/next-prerender-random#render-on-the-client', + link: 'https://nextjs.org/docs/messages/blocking-prerender-random#render-on-the-client', snippets: [ { text: '"use client"', highlight: true }, { text: '// runs in the browser' }, { text: 'const id = Math.random()' }, ], + prompt: + 'Move the component that calls Math.random() into a Client Component by adding "use client" at the top of the file. The browser produces a fresh value on each visit. If the value needs to be hydration-stable, compute it inside a useEffect or event handler instead of inline during render.', }, ] @@ -335,45 +378,53 @@ const syncDateCards: FixCard[] = [ id: 'render-at-request-time', title: 'Generate on every request', group: 'dynamic', - link: 'https://nextjs.org/docs/messages/next-prerender-current-time#render-at-request-time', + link: 'https://nextjs.org/docs/messages/blocking-prerender-current-time#generate-on-every-request', snippets: [ { text: 'await connection()', highlight: true }, { text: 'const t = Date.now()' }, { text: 'return ' }, ], + prompt: + 'Add "await connection()" from "next/server" immediately before the Date.now() call. This marks the component as request-time, so Next.js excludes it from the prerendered HTML and streams it in from the nearest boundary on each request. Do not change the call site of Date.now() itself. Only change the call site once you\'ve confirmed with the user that a fresh value on every request is the intent.', }, { id: 'cache-the-timestamp', title: 'Cache the timestamp', group: 'cache', - link: 'https://nextjs.org/docs/messages/next-prerender-current-time#cache-the-timestamp', + link: 'https://nextjs.org/docs/messages/blocking-prerender-current-time#cache-the-timestamp', snippets: [ { text: 'function Timestamp() {' }, { text: ' "use cache"', highlight: true }, { text: ' return ' }, ], + prompt: + 'Move the Date.now() call into its own function and add "use cache" as the first statement. Optionally call cacheLife(profile) to control how often the timestamp is regenerated. Do not introduce new imports beyond "next/cache".', }, { id: 'render-on-the-client', title: 'Render on the client', group: 'client', - link: 'https://nextjs.org/docs/messages/next-prerender-current-time#render-on-the-client', + link: 'https://nextjs.org/docs/messages/blocking-prerender-current-time#render-on-the-client', snippets: [ { text: '"use client"', highlight: true }, { text: '// runs in the browser' }, { text: 'const t = Date.now()' }, ], + prompt: + 'Move the component that calls Date.now() into a Client Component by adding "use client" at the top of the file. If the value needs to be hydration-stable, compute it inside useEffect instead of inline during render.', }, { id: 'measure-elapsed-time', title: 'For telemetry, use a timing API', group: 'measure', - link: 'https://nextjs.org/docs/messages/next-prerender-current-time#measure-elapsed-time', + link: 'https://nextjs.org/docs/messages/blocking-prerender-current-time#for-telemetry-use-a-timing-api', snippets: [ { text: 'const start = performance.now()', highlight: true }, { text: 'doWork()' }, { text: 'const ms = performance.now() - start' }, ], + prompt: + 'Replace Date.now() with performance.now() if the value is used for elapsed-time measurement, instrumentation, or telemetry. performance.now() returns a high-resolution monotonic timestamp and does not interfere with prerendering. Do not change the call if the value is rendered into the UI.', }, ] @@ -382,34 +433,40 @@ const syncCryptoCards: FixCard[] = [ id: 'render-at-request-time', title: 'Generate on every request', group: 'dynamic', - link: 'https://nextjs.org/docs/messages/next-prerender-crypto#render-at-request-time', + link: 'https://nextjs.org/docs/messages/blocking-prerender-crypto#generate-on-every-request', snippets: [ { text: 'await connection()', highlight: true }, { text: 'const id = crypto.randomUUID()' }, { text: 'return ' }, ], + prompt: + 'Add "await connection()" from "next/server" immediately before the crypto call. This marks the component as request-time, so Next.js excludes it from the prerendered HTML and streams it in from the nearest boundary on each request. Do not change the crypto call itself. Only change the call site once you\'ve confirmed with the user that a fresh value on every request is the intent.', }, { id: 'cache-the-generated-value', title: 'Cache the generated value', group: 'cache', - link: 'https://nextjs.org/docs/messages/next-prerender-crypto#cache-the-generated-value', + link: 'https://nextjs.org/docs/messages/blocking-prerender-crypto#cache-the-generated-value', snippets: [ { text: 'function TokenId() {' }, { text: ' "use cache"', highlight: true }, { text: ' return crypto.randomUUID()' }, ], + prompt: + 'Move the crypto call into its own function and add "use cache" as the first statement. Useful when the same generated value is reused as a key for another cached operation (talking to a database, signing a payload). Do not introduce new imports beyond "next/cache".', }, { id: 'render-on-the-client', title: 'Render on the client', group: 'client', - link: 'https://nextjs.org/docs/messages/next-prerender-crypto#render-on-the-client', + link: 'https://nextjs.org/docs/messages/blocking-prerender-crypto#render-on-the-client', snippets: [ { text: '"use client"', highlight: true }, { text: '// runs in the browser' }, { text: 'const id = crypto.randomUUID()' }, ], + prompt: + 'Move the component that calls the crypto API into a Client Component by adding "use client" at the top of the file. The browser produces the value, so the server never has to. If the value needs to be hydration-stable, compute it inside useEffect instead of inline during render.', }, ] @@ -420,34 +477,40 @@ const syncClientDateCards: FixCard[] = [ id: 'wrap-in-or-move-into-suspense', title: 'Wrap in or move into Suspense', group: 'stream', - link: 'https://nextjs.org/docs/messages/next-prerender-current-time-client#wrap-in-or-move-into-suspense', + link: 'https://nextjs.org/docs/messages/blocking-prerender-current-time-client#wrap-in-or-move-into-suspense', snippets: [ { text: '', highlight: true }, { text: ' ' }, { text: '', highlight: true }, ], + prompt: + 'Wrap the Client Component that calls Date.now() in in its parent. The fallback prop must render synchronous, deterministic JSX (no Date.now or Math.random) that approximates the final layout. Import Suspense from "react". Do not change the Date.now() call.', }, { id: 'move-into-effect-or-event-handler', title: 'Move into effect or event handler', group: 'defer', - link: 'https://nextjs.org/docs/messages/next-prerender-current-time-client#move-into-effect-or-event-handler', + link: 'https://nextjs.org/docs/messages/blocking-prerender-current-time-client#move-into-effect-or-event-handler', snippets: [ { text: '