Skip to content

Commit d60d235

Browse files
authored
[Cache Components] Disable static indicator (#84639)
When `experimental.cacheComponents` is enabled, the static indicator in the Next.js DevTools is now disabled. With Cache Components enabled, the binary static/dynamic state is no longer accurate since routes are typically partially static and dynamic (unless fully opted into dynamic rendering via a Suspense boundary above the body, which is an edge case). To avoid confusion, we're removing it for now, when the feature flag is enabled. In the future, we will likely provide better tools that will allow users to understand which parts of a page are prerendered and which parts are dynamic. In addition, this PR also improves the behavior for fully static App Router pages (without Cache Components). Previously, we would show "Dynamic" as default, and only flipped to "Static" when the request was finished. Now, we're showing a spinner while the page is loading. ![static indicator](https://github.com/user-attachments/assets/c720975d-932c-48b0-b705-7c408fdcf51d) closes NAR-428
1 parent ab09e87 commit d60d235

File tree

35 files changed

+553
-260
lines changed

35 files changed

+553
-260
lines changed

packages/next/src/client/app-index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export function hydrate(
260260
const { createWebSocket } =
261261
require('./dev/hot-reloader/app/web-socket') as typeof import('./dev/hot-reloader/app/web-socket')
262262

263-
staticIndicatorState = { pathname: null, appIsrManifest: {} }
263+
staticIndicatorState = { pathname: null, appIsrManifest: null }
264264
webSocket = createWebSocket(assetPrefix, staticIndicatorState)
265265
}
266266

packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { getOrCreateDebugChannelReadableWriterPair } from '../../debug-channel'
4141

4242
export interface StaticIndicatorState {
4343
pathname: string | null
44-
appIsrManifest: Record<string, true>
44+
appIsrManifest: Record<string, boolean> | null
4545
}
4646

4747
let mostRecentCompilationHash: any = null
@@ -261,18 +261,18 @@ export function processMessage(
261261
if (process.env.__NEXT_DEV_INDICATOR) {
262262
staticIndicatorState.appIsrManifest = message.data
263263

264-
// handle initial status on receiving manifest
265-
// navigation is handled in useEffect for pathname changes
266-
// as we'll receive the updated manifest before usePathname
267-
// triggers for new value
268-
if (
269-
staticIndicatorState.pathname &&
270-
staticIndicatorState.pathname in message.data
271-
) {
272-
dispatcher.onStaticIndicator(true)
273-
} else {
274-
dispatcher.onStaticIndicator(false)
275-
}
264+
// Handle the initial static indicator status on receiving the ISR
265+
// manifest. Navigation is handled in an effect inside HotReload for
266+
// pathname changes as we'll receive the updated manifest before
267+
// usePathname triggers for a new value.
268+
269+
const isStatic = staticIndicatorState.pathname
270+
? message.data[staticIndicatorState.pathname]
271+
: undefined
272+
273+
dispatcher.onStaticIndicator(
274+
isStatic === undefined ? 'pending' : isStatic ? 'static' : 'dynamic'
275+
)
276276
}
277277
break
278278
}
@@ -542,10 +542,14 @@ export default function HotReload({
542542

543543
staticIndicatorState.pathname = pathname
544544

545-
if (pathname && pathname in staticIndicatorState.appIsrManifest) {
546-
dispatcher.onStaticIndicator(true)
547-
} else {
548-
dispatcher.onStaticIndicator(false)
545+
if (staticIndicatorState.appIsrManifest) {
546+
const isStatic = pathname
547+
? staticIndicatorState.appIsrManifest[pathname]
548+
: undefined
549+
550+
dispatcher.onStaticIndicator(
551+
isStatic === undefined ? 'pending' : isStatic ? 'static' : 'dynamic'
552+
)
549553
}
550554
}, [pathname, staticIndicatorState])
551555
}

packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,10 @@ export function handleStaticIndicator() {
265265
appComponent?.getInitialProps !== appComponent?.origGetInitialProps
266266

267267
const isPageStatic =
268-
window.location.pathname in isrManifest ||
268+
isrManifest[window.location.pathname] ||
269269
(!isDynamicPage && !hasAppGetInitialProps)
270270

271-
dispatcher.onStaticIndicator(isPageStatic)
271+
dispatcher.onStaticIndicator(isPageStatic ? 'static' : 'dynamic')
272272
}
273273
}
274274

packages/next/src/next-devtools/dev-overlay.browser.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export interface Dispatcher {
5555
onDebugInfo(debugInfo: DebugInfo): void
5656
onBeforeRefresh(): void
5757
onRefresh(): void
58-
onStaticIndicator(status: boolean): void
58+
onStaticIndicator(status: 'pending' | 'static' | 'dynamic' | 'disabled'): void
5959
onDevIndicator(devIndicator: DevIndicatorServerState): void
6060
onDevToolsConfig(config: DevToolsConfig): void
6161
onUnhandledError(reason: Error): void
@@ -147,9 +147,14 @@ export const dispatcher: Dispatcher = {
147147
dispatch({ type: ACTION_VERSION_INFO, versionInfo })
148148
}
149149
),
150-
onStaticIndicator: createQueuable((dispatch: Dispatch, status: boolean) => {
151-
dispatch({ type: ACTION_STATIC_INDICATOR, staticIndicator: status })
152-
}),
150+
onStaticIndicator: createQueuable(
151+
(
152+
dispatch: Dispatch,
153+
status: 'pending' | 'static' | 'dynamic' | 'disabled'
154+
) => {
155+
dispatch({ type: ACTION_STATIC_INDICATOR, staticIndicator: status })
156+
}
157+
),
153158
onDebugInfo: createQueuable((dispatch: Dispatch, debugInfo: DebugInfo) => {
154159
dispatch({ type: ACTION_DEBUG_INFO, debugInfo })
155160
}),
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export function LoadingIcon() {
2+
return (
3+
<svg
4+
width="20px"
5+
height="20px"
6+
viewBox="0 0 20 20"
7+
fill="none"
8+
xmlns="http://www.w3.org/2000/svg"
9+
>
10+
<circle
11+
cx="10"
12+
cy="10"
13+
r="7"
14+
stroke="currentColor"
15+
strokeWidth="2"
16+
strokeLinecap="round"
17+
strokeDasharray="32 12"
18+
opacity="0.8"
19+
>
20+
<animateTransform
21+
attributeName="transform"
22+
type="rotate"
23+
from="0 10 10"
24+
to="360 10 10"
25+
dur="1s"
26+
repeatCount="indefinite"
27+
/>
28+
</circle>
29+
</svg>
30+
)
31+
}

packages/next/src/next-devtools/dev-overlay/menu/panel-router.tsx

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ACTION_ERROR_OVERLAY_OPEN,
2525
} from '../shared'
2626
import GearIcon from '../icons/gear-icon'
27+
import { LoadingIcon } from '../icons/loading-icon'
2728
import { UserPreferencesBody } from '../components/errors/dev-tools-indicator/dev-tools-info/user-preferences'
2829
import { useShortcuts } from '../hooks/use-shortcuts'
2930
import { useUpdateAllPanelPositions } from '../components/devtools-indicator/devtools-indicator'
@@ -60,17 +61,24 @@ const MenuPanel = () => {
6061
}
6162
},
6263
},
63-
{
64-
title: `Current route is ${state.staticIndicator ? 'static' : 'dynamic'}.`,
65-
label: 'Route',
66-
value: state.staticIndicator ? 'Static' : 'Dynamic',
67-
onClick: () => setPanel('route-type'),
68-
attributes: {
69-
'data-nextjs-route-type': state.staticIndicator
70-
? 'static'
71-
: 'dynamic',
72-
},
73-
},
64+
state.staticIndicator === 'disabled'
65+
? undefined
66+
: state.staticIndicator === 'pending'
67+
? {
68+
title: 'Loading...',
69+
label: 'Route',
70+
value: <LoadingIcon />,
71+
}
72+
: {
73+
title: `Current route is ${state.staticIndicator}.`,
74+
label: 'Route',
75+
value:
76+
state.staticIndicator === 'static' ? 'Static' : 'Dynamic',
77+
onClick: () => setPanel('route-type'),
78+
attributes: {
79+
'data-nextjs-route-type': state.staticIndicator,
80+
},
81+
},
7482
!!process.env.TURBOPACK
7583
? {
7684
title: 'Turbopack is enabled.',
@@ -166,39 +174,39 @@ export const PanelRouter = () => {
166174
</DynamicPanel>
167175
</PanelRoute>
168176

169-
<PanelRoute name="route-type">
170-
<DynamicPanel
171-
key={state.staticIndicator ? 'static' : 'dynamic'}
172-
sharePanelSizeGlobally={false}
173-
sizeConfig={{
174-
kind: 'fixed',
175-
height: state.staticIndicator
176-
? 300 / state.scale
177-
: 325 / state.scale,
178-
width: 400 / state.scale,
179-
}}
180-
closeOnClickOutside
181-
header={
182-
<DevToolsHeader
183-
title={`${state.staticIndicator ? 'Static' : 'Dynamic'} Route`}
184-
/>
185-
}
186-
>
187-
<div className="panel-content">
188-
<RouteInfoBody
189-
routerType={state.routerType}
190-
isStaticRoute={state.staticIndicator}
191-
/>
192-
<InfoFooter
193-
href={
194-
learnMoreLink[state.routerType][
195-
state.staticIndicator ? 'static' : 'dynamic'
196-
]
177+
{state.staticIndicator !== 'disabled' &&
178+
state.staticIndicator !== 'pending' && (
179+
<PanelRoute name="route-type">
180+
<DynamicPanel
181+
key={state.staticIndicator}
182+
sharePanelSizeGlobally={false}
183+
sizeConfig={{
184+
kind: 'fixed',
185+
height:
186+
state.staticIndicator === 'static'
187+
? 300 / state.scale
188+
: 325 / state.scale,
189+
width: 400 / state.scale,
190+
}}
191+
closeOnClickOutside
192+
header={
193+
<DevToolsHeader
194+
title={`${state.staticIndicator === 'static' ? 'Static' : 'Dynamic'} Route`}
195+
/>
197196
}
198-
/>
199-
</div>
200-
</DynamicPanel>
201-
</PanelRoute>
197+
>
198+
<div className="panel-content">
199+
<RouteInfoBody
200+
routerType={state.routerType}
201+
isStaticRoute={state.staticIndicator === 'static'}
202+
/>
203+
<InfoFooter
204+
href={learnMoreLink[state.routerType][state.staticIndicator]}
205+
/>
206+
</div>
207+
</DynamicPanel>
208+
</PanelRoute>
209+
)}
202210

203211
{isAppRouter && (
204212
<PanelRoute name="segment-explorer">

packages/next/src/next-devtools/dev-overlay/shared.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export interface OverlayState {
4949
readonly notFound: boolean
5050
readonly buildingIndicator: boolean
5151
readonly renderingIndicator: boolean
52-
readonly staticIndicator: boolean
52+
readonly staticIndicator: 'pending' | 'static' | 'dynamic' | 'disabled'
5353
readonly showIndicator: boolean
5454
readonly disableDevIndicator: boolean
5555
readonly debugInfo: DebugInfo
@@ -112,7 +112,7 @@ export const ACTION_DEVTOOL_UPDATE_ROUTE_STATE =
112112

113113
interface StaticIndicatorAction {
114114
type: typeof ACTION_STATIC_INDICATOR
115-
staticIndicator: boolean
115+
staticIndicator: 'pending' | 'static' | 'dynamic' | 'disabled'
116116
}
117117

118118
interface BuildOkAction {
@@ -263,7 +263,7 @@ export const INITIAL_OVERLAY_STATE: Omit<
263263
errors: [],
264264
notFound: false,
265265
renderingIndicator: false,
266-
staticIndicator: false,
266+
staticIndicator: 'disabled',
267267
/*
268268
This is set to `true` when we can reliably know
269269
whether the indicator is in disabled state or not.

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,6 +1502,7 @@ async function renderToHTMLOrFlightImpl(
15021502
serverActions,
15031503
assetPrefix = '',
15041504
enableTainting,
1505+
experimental,
15051506
} = renderOpts
15061507

15071508
// We need to expose the bundled `require` API globally for
@@ -1566,10 +1567,18 @@ async function renderToHTMLOrFlightImpl(
15661567
globalThis.__next_chunk_load__ = __next_chunk_load__
15671568
}
15681569

1569-
if (process.env.NODE_ENV === 'development') {
1570-
// reset isr status at start of request
1570+
if (
1571+
process.env.NODE_ENV === 'development' &&
1572+
renderOpts.setIsrStatus &&
1573+
!experimental.cacheComponents
1574+
) {
1575+
// Reset the ISR status at start of request.
15711576
const { pathname } = new URL(req.url || '/', 'http://n')
1572-
renderOpts.setIsrStatus?.(pathname, false)
1577+
renderOpts.setIsrStatus(
1578+
pathname,
1579+
// Only pages using the Node runtime can use ISR, Edge is always dynamic.
1580+
process.env.NEXT_RUNTIME === 'edge' ? false : undefined
1581+
)
15731582
}
15741583

15751584
if (
@@ -1844,19 +1853,19 @@ async function renderToHTMLOrFlightImpl(
18441853
if (
18451854
process.env.NODE_ENV === 'development' &&
18461855
renderOpts.setIsrStatus &&
1856+
!experimental.cacheComponents &&
1857+
// Only pages using the Node runtime can use ISR, so we only need to
1858+
// update the status for those.
18471859
// The type check here ensures that `req` is correctly typed, and the
18481860
// environment variable check provides dead code elimination.
18491861
process.env.NEXT_RUNTIME !== 'edge' &&
1850-
isNodeNextRequest(req) &&
1851-
!isDevWarmupRequest
1862+
isNodeNextRequest(req)
18521863
) {
18531864
const setIsrStatus = renderOpts.setIsrStatus
18541865
req.originalRequest.on('end', () => {
1855-
if (!requestStore.usedDynamic && !workStore.forceDynamic) {
1856-
// only node can be ISR so we only need to update the status here
1857-
const { pathname } = new URL(req.url || '/', 'http://n')
1858-
setIsrStatus(pathname, true)
1859-
}
1866+
const { pathname } = new URL(req.url || '/', 'http://n')
1867+
const isStatic = !requestStore.usedDynamic && !workStore.forceDynamic
1868+
setIsrStatus(pathname, isStatic)
18601869
})
18611870
}
18621871

packages/next/src/server/app-render/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export interface RenderOptsPartial {
9393
}
9494
isOnDemandRevalidate?: boolean
9595
isPossibleServerAction?: boolean
96-
setIsrStatus?: (key: string, value: boolean) => void
96+
setIsrStatus?: (key: string, value: boolean | undefined) => void
9797
setReactDebugChannel?: (
9898
debugChannel: { readable: ReadableStream<Uint8Array> },
9999
htmlRequestId: string,

0 commit comments

Comments
 (0)