mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Disable instant validations in draft mode (#93472)
This PR makes dev-mode cache bypass behavior consistent when `draftMode` is enabled. Draft mode now skips Instant Insights validation the same way a hard refresh or DevTools “Disable Cache” request does, and the Next.js devtools badge shows the existing “Cache disabled” state for draft-mode previews. It also adds coverage for draft-mode cache bypass behavior in dev and start modes, plus a devtools badge test, and updates the `cache-bypass-in-dev` docs to explain why draft mode triggers this state.
This commit is contained in:
@@ -4,27 +4,32 @@ title: Caches Bypassed in Development Mode
|
||||
|
||||
## Disabling Caches in Development Mode
|
||||
|
||||
While running Next.js in development mode with `next dev` an initial page load or client navigation was handled with a `cache-control: no-store` header. The most common reason for this is that you have your web browser's DevTools open with "Disable Cache" enabled. Another common reason is you performed a hard refresh (Cmd + Shift + R / Ctrl + Shift + R).
|
||||
While running Next.js in development mode with `next dev`, an initial page load or client navigation was handled with server caches bypassed. This can happen when the request includes a `cache-control: no-cache` header. The most common reason for this is that you have your web browser's DevTools open with "Disable Cache" enabled. Another common reason is you performed a hard refresh (Cmd + Shift + R / Ctrl + Shift + R).
|
||||
|
||||
Disabling caches while in development mode affects the debugging experience of Cache Components. Read on to understand more about how Next.js handles caches in development mode and what changes when they are intentionally disabled.
|
||||
This can also happen when [`draftMode`](/docs/app/api-reference/functions/draft-mode) is enabled. Draft Mode is designed to bypass caches so you can preview live or unpublished content before it is published to the cached or prerendered version of your site.
|
||||
|
||||
## Understanding Cache handling in Development Mode
|
||||
Bypassing caches while in development mode affects the debugging experience of Cache Components and Instant Insights. Read on to understand more about how Next.js handles caches in development mode and what changes when they are intentionally bypassed.
|
||||
|
||||
In development mode, Next.js ensures that Cache Components render in the right environment to allow for React DevTools to highlight which components are part of the available for prerendering statically before any user request, which are available when prefetching segments at runtime before a navigation occurs, and finally which are dynamic and will only be fetched on user navigation.
|
||||
## Understanding Cache Handling in Development Mode
|
||||
|
||||
This behavior has a small cost when a cold cache is encountered. Rather than streaming immediately Next.js will wait to fill this cache before streaming the response. As you navigate around you App in development mode these caches will fill and most navigations to cached content should feel instant once warm.
|
||||
In development mode, Next.js ensures that Cache Components render in the right environment so React DevTools can highlight which components are available for static prerendering before any user request, which are available when prefetching segments at runtime before a navigation occurs, and which are dynamic and only fetched during user navigation.
|
||||
|
||||
However if you instruct Next.js to skip all server caches using something like the "Disable Cache" option in your browser's DevTools, this behavior would be detrimental since every cache will be considered cold on every request leading to blocking navigations. Instead Next.js prints a warning that this particular request is not suitable for debugging Cache Components and then renders fresh results as fast as possible.
|
||||
This behavior has a small cost when a cold cache is encountered. Rather than streaming immediately, Next.js waits to fill this cache before streaming the response. As you navigate around your app in development mode, these caches will fill and most navigations to cached content should feel instant once warm.
|
||||
|
||||
## Should you use "Disable Cache"?
|
||||
However, if you instruct Next.js to skip all server caches using something like the "Disable Cache" option in your browser's DevTools, or by enabling Draft Mode, this behavior would be detrimental since every cache will be considered cold on every request, leading to blocking navigations. Instead, Next.js prints a warning that this particular request is not suitable for debugging Cache Components or Instant Insights and then renders fresh results as fast as possible.
|
||||
|
||||
Chrome, in particular, defaults all network request to "Disable Cache" when DevTools is open. For Next.js this is not recommended because it is overly aggressive in what is not cached and can provide a false impression of page performance that doesn't represent realistic usage of a web app.
|
||||
## Should You Bypass Caches?
|
||||
|
||||
However there are times when disabling a server caches can be useful. For instance if you know some upstream data system has updated but these updates are not connected to your local dev environment through features like `revalidateTag`.
|
||||
Chrome, in particular, defaults all network requests to "Disable Cache" when DevTools is open. For Next.js this is not recommended because it is overly aggressive in what is not cached and can provide a false impression of page performance that doesn't represent realistic usage of a web app.
|
||||
|
||||
If you must turn on "Disable Cache" to force a refresh we recommend that you disable the option once you've achieved your goal.
|
||||
However, there are times when bypassing server caches can be useful. For instance, if you know some upstream data system has updated but these updates are not connected to your local dev environment through features like `revalidateTag`, a hard refresh or "Disable Cache" can force a fresh render.
|
||||
|
||||
If you must turn on "Disable Cache" to force a refresh, we recommend that you disable the option once you've achieved your goal.
|
||||
|
||||
If this warning appears while Draft Mode is enabled, it is expected. Draft Mode intentionally bypasses `"use cache"` and other server caches so your preview can render the latest content dynamically. Disable Draft Mode when you want to debug what can be prerendered, prefetched, or cached for normal visitors.
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [`draftMode` function](/docs/app/api-reference/functions/draft-mode)
|
||||
- [`revalidateTag` function](/docs/app/api-reference/functions/revalidateTag)
|
||||
- [`"use cache"` directive](/docs/app/api-reference/directives/use-cache)
|
||||
|
||||
@@ -40,13 +40,15 @@ export function tryGetPreviewData(
|
||||
|
||||
const previewModeId = cookies.get(COOKIE_NAME_PRERENDER_BYPASS)?.value
|
||||
const tokenPreviewData = cookies.get(COOKIE_NAME_PRERENDER_DATA)?.value
|
||||
const isValidPreviewModeId =
|
||||
previewModeId === options.previewModeId ||
|
||||
// In dev mode, the cookie can be the actual hash value preview id but the
|
||||
// preview props can still be `development-id`.
|
||||
(process.env.NODE_ENV !== 'production' &&
|
||||
options.previewModeId === 'development-id')
|
||||
|
||||
// Case: preview mode cookie set but data cookie is not set
|
||||
if (
|
||||
previewModeId &&
|
||||
!tokenPreviewData &&
|
||||
previewModeId === options.previewModeId
|
||||
) {
|
||||
if (previewModeId && !tokenPreviewData && isValidPreviewModeId) {
|
||||
// This is "Draft Mode" which doesn't use
|
||||
// previewData, so we return an empty object
|
||||
// for backwards compat with "Preview Mode".
|
||||
|
||||
@@ -662,12 +662,17 @@ async function generateDynamicRSCPayload(
|
||||
|
||||
if (!options?.skipPageRendering) {
|
||||
const preloadCallbacks: PreloadCallbacks = []
|
||||
const requestStore = workUnitAsyncStorage.getStore()
|
||||
|
||||
// If we're performing instant validation, we need to render the whole tree,
|
||||
// without skipping shared layouts.
|
||||
const needsFullTree =
|
||||
process.env.__NEXT_DEV_SERVER &&
|
||||
ctx.renderOpts.cacheComponents &&
|
||||
!(
|
||||
requestStore?.type === 'request' &&
|
||||
isBypassingCachesInDev(requestStore, workStore)
|
||||
) &&
|
||||
!options?.actionResult && // Only for navigations
|
||||
(await anySegmentNeedsInstantValidationInDev(loaderTree))
|
||||
|
||||
@@ -1482,7 +1487,7 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
|
||||
// instant configs exist, since we render all the layouts necessary to perform
|
||||
// the validation in those cases.
|
||||
const shouldValidate =
|
||||
!isBypassingCachesInDev(initialRequestStore) &&
|
||||
!isBypassingCachesInDev(initialRequestStore, workStore) &&
|
||||
(initialRequestStore.isHmrRefresh === true ||
|
||||
(await anySegmentNeedsInstantValidationInDev(loaderTree)))
|
||||
|
||||
@@ -1496,7 +1501,7 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
|
||||
undefined
|
||||
)
|
||||
|
||||
if (isBypassingCachesInDev(requestStore)) {
|
||||
if (isBypassingCachesInDev(requestStore, workStore)) {
|
||||
// Mark the RSC payload to indicate that caches were bypassed in dev.
|
||||
// This lets the client know not to cache anything based on this render.
|
||||
payload._bypassCachesInDev = createElement(WarnForBypassCachesInDev, {
|
||||
@@ -1519,8 +1524,9 @@ async function generateDynamicFlightRenderResultWithStagesInDev(
|
||||
// (which is not the case for renders after an action)
|
||||
createRequestStore &&
|
||||
// We only do this flow if we're not bypassing caches in dev using
|
||||
// "disable cache" in devtools or a hard refresh (cache-control: "no-store")
|
||||
!isBypassingCachesInDev(initialRequestStore)
|
||||
// "disable cache" in devtools, a hard refresh (cache-control: "no-cache"),
|
||||
// or draft mode.
|
||||
!isBypassingCachesInDev(initialRequestStore, workStore)
|
||||
) {
|
||||
// Before we kick off the render, we set the cache status back to it's initial state
|
||||
// in case a previous render bypassed the cache.
|
||||
@@ -3439,7 +3445,7 @@ async function renderToStream(
|
||||
{ is404: res.statusCode === 404 }
|
||||
)
|
||||
|
||||
if (isBypassingCachesInDev(requestStore)) {
|
||||
if (isBypassingCachesInDev(requestStore, workStore)) {
|
||||
// Mark the RSC payload to indicate that caches were bypassed in dev.
|
||||
// This lets the client know not to cache anything based on this render.
|
||||
if (renderOpts.setCacheStatus) {
|
||||
@@ -3462,8 +3468,9 @@ async function renderToStream(
|
||||
// (which is not the case for renders after an action)
|
||||
createRequestStore &&
|
||||
// We only do this flow if we're not bypassing caches in dev using
|
||||
// "disable cache" in devtools or a hard refresh (cache-control: "no-store")
|
||||
!isBypassingCachesInDev(requestStore)
|
||||
// "disable cache" in devtools, a hard refresh (cache-control: "no-cache"),
|
||||
// or draft mode.
|
||||
!isBypassingCachesInDev(requestStore, workStore)
|
||||
) {
|
||||
const {
|
||||
stream: serverStream,
|
||||
@@ -3557,7 +3564,7 @@ async function renderToStream(
|
||||
{ is404: res.statusCode === 404 }
|
||||
)
|
||||
|
||||
if (isBypassingCachesInDev(requestStore)) {
|
||||
if (isBypassingCachesInDev(requestStore, workStore)) {
|
||||
// Mark the RSC payload to indicate that caches were bypassed in dev.
|
||||
// This lets the client know not to cache anything based on this render.
|
||||
if (renderOpts.setCacheStatus) {
|
||||
@@ -3580,8 +3587,9 @@ async function renderToStream(
|
||||
// (which is not the case for renders after an action)
|
||||
createRequestStore &&
|
||||
// We only do this flow if we're not bypassing caches in dev using
|
||||
// "disable cache" in devtools or a hard refresh (cache-control: "no-store")
|
||||
!isBypassingCachesInDev(requestStore)
|
||||
// "disable cache" in devtools, a hard refresh (cache-control: "no-cache"),
|
||||
// or draft mode.
|
||||
!isBypassingCachesInDev(requestStore, workStore)
|
||||
) {
|
||||
const {
|
||||
stream: serverStream,
|
||||
@@ -8761,10 +8769,15 @@ async function collectSegmentData(
|
||||
)
|
||||
}
|
||||
|
||||
function isBypassingCachesInDev(requestStore: RequestStore): boolean {
|
||||
function isBypassingCachesInDev(
|
||||
requestStore: RequestStore,
|
||||
workStore: WorkStore
|
||||
): boolean {
|
||||
return (
|
||||
!!process.env.__NEXT_DEV_SERVER &&
|
||||
requestStore.headers.get('cache-control') === 'no-cache'
|
||||
(requestStore.headers.get('cache-control') === 'no-cache' ||
|
||||
requestStore.draftMode.isEnabled ||
|
||||
workStore.isDraftMode === true)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function GET() {
|
||||
;(await draftMode()).enable()
|
||||
redirect('/')
|
||||
}
|
||||
@@ -70,6 +70,34 @@ describe('cache-indicator', () => {
|
||||
expect(badgeText).toBe('Cache disabled')
|
||||
})
|
||||
|
||||
it('shows cache-bypassing badge when draft mode is enabled', async () => {
|
||||
const browser = await next.browser('/api/draft/enable')
|
||||
|
||||
// Wait for the badge to appear and show cache-bypassing status
|
||||
await retry(async () => {
|
||||
const badge = await browser.elementByCss('[data-next-badge]')
|
||||
const cacheBypassingAttr = await badge.getAttribute(
|
||||
'data-cache-bypassing'
|
||||
)
|
||||
expect(cacheBypassingAttr).toBe('true')
|
||||
})
|
||||
|
||||
// Verify the cache bypass badge is visible
|
||||
await retry(async () => {
|
||||
const hasCacheBypassBadge = await browser.hasElementByCss(
|
||||
'[data-cache-bypass-badge]'
|
||||
)
|
||||
expect(hasCacheBypassBadge).toBe(true)
|
||||
})
|
||||
|
||||
// Verify the badge shows "Cache disabled" text
|
||||
const badgeButton = await browser.elementByCss(
|
||||
'[data-cache-bypass-badge] [data-issues-open]'
|
||||
)
|
||||
const badgeText = await badgeButton.text()
|
||||
expect(badgeText).toBe('Cache disabled')
|
||||
})
|
||||
|
||||
it('persists cache-bypassing badge after navigation when cache is disabled', async () => {
|
||||
const browser = await next.browser('/', {
|
||||
extraHTTPHeaders: { 'cache-control': 'no-cache' },
|
||||
|
||||
@@ -10,6 +10,17 @@ describe('Cache Components Errors', () => {
|
||||
}
|
||||
|
||||
describe('Warning for Bypassing Caches in Dev', () => {
|
||||
async function enableDraftMode() {
|
||||
const draftRes = await next.fetch('/api/draft/enable', {
|
||||
redirect: 'manual',
|
||||
})
|
||||
const setCookie = draftRes.headers.get('set-cookie')
|
||||
const cookie = setCookie?.split(';', 1)[0]
|
||||
|
||||
expect(cookie).toBeTruthy()
|
||||
return cookie!
|
||||
}
|
||||
|
||||
if (isNextDev) {
|
||||
it('warns if you render with cache-control: no-cache in dev on initial page load', async () => {
|
||||
// Wait for pages to be loaded to ensure the logging for the test page is the only thing that can be logged.
|
||||
@@ -68,6 +79,23 @@ describe('Cache Components Errors', () => {
|
||||
`""`
|
||||
)
|
||||
})
|
||||
|
||||
it('warns if draftMode is enabled in dev', async () => {
|
||||
// Wait for pages to be loaded to ensure the logging for the test page is the only thing that can be logged.
|
||||
await next.fetch('/404')
|
||||
const from = next.cliOutput.length
|
||||
const cookie = await enableDraftMode()
|
||||
|
||||
await next.fetch('/draft', {
|
||||
headers: { Cookie: cookie },
|
||||
})
|
||||
|
||||
expect(stripGetLines(next.cliOutput.slice(from)))
|
||||
.toMatchInlineSnapshot(`
|
||||
"Route /draft is rendering with server caches disabled. For this navigation, Component Metadata in React DevTools will not accurately reflect what is statically prerenderable and runtime prefetchable. See more info here: https://nextjs.org/docs/messages/cache-bypass-in-dev
|
||||
"
|
||||
`)
|
||||
})
|
||||
} else {
|
||||
it('does not warn if you render with cache-control: no-cache in dev on initial page load', async () => {
|
||||
// Wait for pages to be loaded to ensure the logging for the test page is the only thing that can be logged.
|
||||
@@ -122,6 +150,21 @@ describe('Cache Components Errors', () => {
|
||||
`""`
|
||||
)
|
||||
})
|
||||
|
||||
it('does not warn if draftMode is enabled in start', async () => {
|
||||
// Wait for pages to be loaded to ensure the logging for the test page is the only thing that can be logged.
|
||||
await next.fetch('/404')
|
||||
const from = next.cliOutput.length
|
||||
const cookie = await enableDraftMode()
|
||||
|
||||
await next.fetch('/draft', {
|
||||
headers: { Cookie: cookie },
|
||||
})
|
||||
|
||||
expect(stripGetLines(next.cliOutput.slice(from))).toMatchInlineSnapshot(
|
||||
`""`
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { draftMode } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const redirectTo = new URL(request.url).searchParams.get('redirect') ?? '/'
|
||||
|
||||
;(await draftMode()).enable()
|
||||
redirect(redirectTo)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export default async function Page() {
|
||||
return (
|
||||
<main>
|
||||
<section>
|
||||
<CachedData />
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
async function CachedData() {
|
||||
'use cache'
|
||||
|
||||
await new Promise((r) => setTimeout(r, 2000))
|
||||
|
||||
return <p>{99}</p>
|
||||
}
|
||||
Reference in New Issue
Block a user