Next.js encountered runtime data in generateViewport()
This Insight is part of the Instant Navigations feature introduced in Next.js 16.3. If you're new to it, start with the Ensuring instant navigations guide for an overview of what instant navigations are and how Next.js validates them, then come back here for the specific fix.
During prerendering, generateViewport() read a per-request value (cookies(), headers(), params, searchParams). 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.
Uncached data accesses (fetch(), database calls, await connection()) in generateViewport() have different fixes. See Next.js encountered uncached data in generateViewport(). The metadata equivalent is handled at Runtime data in generateMetadata(). For errors in the page body rather than viewport, see Next.js encountered runtime data during prerendering.
Ways to fix this
Use static viewport
Choose this fix when the viewport doesn't actually need the per-request data, either because the values are known at build time, or because the dependency on runtime data is accidental and can be refactored away. Replace generateViewport() with a static viewport export, or rewrite generateViewport() so it no longer reads cookies(), headers(), or other request-bound APIs. The values are evaluated once during the build.
Patterns
Export a static object
Replace the function with a plain object export.
export const viewport = {
themeColor: '#000000',
width: 'device-width',
initialScale: 1,
}
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}Learn more: Static viewport.
Trade-off
Static viewport can't reflect per-request values like a user's preferred theme color stored in a cookie. If the viewport must be personalized, use Allow blocking route.
Gotchas
- The
viewportexport is typically set in the root layout. Changing a layout's viewport affects every route in its subtree. themeColoris the most common reason for a dynamicgenerateViewport. Consider whether a single static value covers all cases before resorting to a blocking route.
Allow blocking route
Choose this fix when the viewport genuinely requires per-request data (a theme color from a cookie, a user-preferred width) and a static export isn't feasible. 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.
Patterns
Opt the layout out
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 { cookies } from 'next/headers'
export const instant = false
export async function generateViewport() {
const cookieJar = await cookies()
return {
themeColor: cookieJar.get('theme-color')?.value ?? '#000',
}
}
export default function DashboardLayout({ children }) {
return children
}Learn more: Route segment instant config.
Use this pattern when:
- The viewport is personalized per user and the value must be correct on the first paint (no flash).
- You're migrating a route incrementally and want to defer the lifetime decision without changing how the page renders today.
Don't use this to dismiss the error. Choose Use static viewport when feasible.
Trade-off
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.
Gotchas
- Setting
instanttofalseopts only the segment that exports it out. Descendant segments are still validated by the global default. - This export does not disable prerendering. The route still prerenders if it can. It only disables instant-navigation validation for the route.
- If the dynamic viewport is the only reason the route blocks, consider whether a static default covers most users. A static
themeColorwith a client-side correction after hydration may give a better experience than blocking the entire navigation. - Framework-synthesized routes (
/_not-found,/_global-error) inherit the root layout'sgenerateViewportand must be statically prerendered.instant = falseopts the route out of validation but does not let those routes through, so the build still fails when they prerender. If your root layout'sgenerateViewportdepends on request data, Use static viewport instead, or move toglobal-not-found.js, which bypasses the root layout entirely and avoids inheriting itsgenerateViewport.
Verifying the fix
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.
Don't want this validation?
Instant-navigation validation runs by default in Cache Components apps and is what surfaces this error.
- One segment: add
export const instant = falseto the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations. - Entire app: set
experimental.instantInsights.validationLevelto'manual-warning'innext.config. This limits validation to segments that explicitly exportinstant.
See Ensuring instant navigations for the full model.
Related Insights
- Runtime data during prerendering
- Uncached data during prerendering
- URL data in a Client Component outside of Suspense
- Runtime data in
generateMetadata() - Uncached data in
generateMetadata() - Uncached data in
generateViewport() Math.random()while prerenderingMath.random()in a Client ComponentDate.now()while prerenderingDate.now()in a Client Component- Crypto APIs while prerendering
- Crypto APIs in a Client Component
- Dynamic data during prefetching
- URL data outside of Suspense
- Unrendered segment
Was this helpful?