errors/blocking-prerender-viewport-dynamic.mdx
During prerendering, generateViewport() performed an uncached data access (fetch(), database call, await connection()). With Cache Components enabled, viewport metadata can't be deferred behind a <Suspense> boundary because it affects the initial page load. The page can't be prerendered, so navigations block instead of being instant.
Request-bound reads (cookies(), headers(), params, searchParams) in generateViewport() have different fixes. See Next.js encountered runtime data in generateViewport(). The metadata equivalent is handled at Uncached data in generateMetadata(). For errors in the page body rather than viewport, see Next.js encountered uncached data during prerendering.
Choose this fix when the viewport values come from an external source (database, CMS) but don't need to change on every request. Add the use cache directive as the first statement inside generateViewport(). Next.js caches the returned viewport object and includes it in the prerender.
This fix does not apply to connection(). The point of connection() is to opt into per-request rendering, so caching it would defeat the purpose. Use Allow blocking route instead.
use cache to generateViewportMark the function as cacheable. The viewport is evaluated once per cache window and reused.
import { db } from './db'
export async function generateViewport() {
'use cache'
const { width, initialScale } = await db.query('viewport-config')
return { width, initialScale }
}
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Learn more: Caching with use cache.
Freshness depends on the cache configuration. The viewport stays the same until cacheLife expires or cacheTag is invalidated.
use cache scope, you can't call cookies() or headers(). If the viewport needs a request-bound value (a theme color from a cookie), use Allow blocking route instead.cacheLife (a profile whose revalidate is shorter than the prerender's effective lifetime) prevents the viewport from being included in the prerender. Use a longer profile if you want the viewport included in the static shell.Choose this fix when the viewport data is genuinely uncacheable. Setting instant to false exempts the segment from instant-navigation validation. The page renders on every request and the navigation blocks until that render completes.
Unlike page body content, viewport metadata can't be deferred behind <Suspense> because it affects the initial HTML <head>. Making the viewport dynamic means the entire page navigation blocks.
Set instant to false on the layout that defines generateViewport. This allows that layout segment to block while descendant segments remain independently validated. Apply this to the nested layout that owns the dynamic viewport, not the root layout, so the opt-out is scoped to the affected segment.
import { db } from './db'
export const instant = false
export async function generateViewport() {
const { width, initialScale } = await db.query('viewport-config')
return { width, initialScale }
}
export default function DashboardLayout({ children }) {
return children
}
Learn more: Route segment instant config.
Use this pattern when:
Don't use this to dismiss the error. Choose Cache the viewport data when feasible.
Navigations to this route are not instant. The user waits for the full server render before any HTML arrives. Use this only when that latency is necessary for the route to function.
instant to false opts only the segment that exports it out. Descendant segments are still validated by the global default./_not-found, /_global-error) inherit the root layout's generateViewport and must be statically prerendered. instant = false opts the route out of validation but does not let those routes through, so the build still fails when they prerender. If your root layout's generateViewport depends on request data, Cache the viewport data instead, or move to global-not-found.js, which bypasses the root layout entirely and avoids inheriting its generateViewport.After applying a fix, reload the route and confirm the page immediately paints meaningful UI, with any <Suspense> fallbacks covering only the regions that stream in. A <Suspense> boundary placed around the whole page body can pass validation with an empty shell, which defeats the point of an instant navigation.
In next dev, the error overlay points at the failing component with file paths and line numbers. When working from a build instead, the default next build output is more abbreviated. Run next build --debug-prerender for full user-frame stack traces and next build --debug-build-paths /dashboard /settings to iterate on specific routes.
Instant-navigation validation runs by default in Cache Components apps and is what surfaces this error.
export const instant = false to the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations.experimental.instantInsights.validationLevel to 'manual-warning' in next.config. This limits validation to segments that explicitly export instant.See Ensuring instant navigations for the full model.
generateMetadata()generateMetadata()generateViewport()Math.random() while prerenderingMath.random() in a Client ComponentDate.now() while prerenderingDate.now() in a Client Component