mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
## What? Adds the Instant Insight + rule page for the Partial Prefetching shell validation (Janka's #95151): with `partialPrefetching` on, a route that reads `params`/`searchParams` outside `<Suspense>` blocks the extraction of the per-route **App Shell** that every link to the route shares. Surfaced as an Instant insight (not a redbox), with two fix cards: Stream (wrap in `<Suspense>`) · Allow blocking route (`instant = false`). The insight fires both at link-prefetch time and during a client-side navigation, so it covers a `<Link prefetch>` into the route as well as a plain client navigation to it. Demos: - [99-link-params-gsp](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/99-link-params-gsp) — `<Link prefetch>` to a `generateStaticParams` route that awaits `params` unguarded. - [117-subnav-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/117-subnav-params) — client-side navigation into a `[slug]` route that reads `params` outside `<Suspense>`. - [118-subnav-search-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/118-subnav-search-params) — client-side navigation into a route that reads `searchParams` outside `<Suspense>`. Follow-up: #95389 documents the route-side audit in the Adopting Partial Prefetching guide (draft, to be coordinated). ## How? - Family-wide polish across all 16 insight pages while aligning the new one: consistent "Verifying the fix" wording, normalized `Learn more:` link texts (target title for guides, bare code name for API references), "App Shell" used consistently, CLS guidance reframed as "minimizing layout shift", prose cleanups (no semicolons, no code-initial sentences), and the insight-error-page skill updated to match. - New `errors/instant-shell-url-data.mdx` rule page, structured like the rest of the insight family (`blocking-prerender-client-hook`, `instant-link-prefetch-partial`); added to every page's Related Insights. New **URL data** glossary entry ("varies per link, not per session"), linked from the page. - `createLinkBodyErrorInNavigation` re-pointed to the page and classified as a navigation-time insight (`inNavigation` → Instant tab) using the shared `linkCards` fix cards. - Common insight format for the message: headline "Next.js encountered URL data outside of Suspense", consequence "may prevent the navigation from being instant, leading to a slower user experience" ("may", since boundaries or a runtime prefetch can still keep the navigation instant), and the two fixes phrased as `[stream] Provide a placeholder with <Suspense fallback={...}> around the data access` and `[block] Set export const instant = false to allow a blocking route`. The shell mechanics are explained on the docs page rather than in the headline. - Second fix reframed from "Disable validation" (group `ignore`) to **Allow blocking route** (group `block`), matching the `blocking-prerender-*` family: `instant = false` marks the segment as allowed to block rather than opting the route out of validation. - Metadata/viewport variants: only the headline term is aligned ("link data" → "URL data") plus a small grammar fix; framing, fix cards, and docs URLs stay on the existing `-metadata-runtime`/`-viewport-runtime` pages unchanged. - `errors.json` regenerated append-only (codes 1407-1409), and the docs opt-out wording updated to the new silence-free phrasing. - e2e snapshots updated for all four configs (dev/start × default/partial-prefetching); unit tests for the matcher, cards, classification, and `isInstantNavigationError`. <!-- NEXT_JS_LLM_PR -->
172 lines
11 KiB
Plaintext
172 lines
11 KiB
Plaintext
---
|
|
title: Next.js encountered runtime data in generateViewport()
|
|
kind: insight
|
|
---
|
|
|
|
<div
|
|
style={{
|
|
padding: '1.25rem 1.5rem',
|
|
border: '1px solid var(--ds-gray-400)',
|
|
borderRadius: '12px',
|
|
background: 'var(--ds-background-200)',
|
|
margin: '1.5rem 0 2rem',
|
|
fontSize: '0.95rem',
|
|
lineHeight: '1.6',
|
|
}}
|
|
>
|
|
This Insight is part of the [Instant
|
|
Navigations](https://nextjs.org/blog/next-16-3-instant-navigations) feature
|
|
introduced in Next.js 16.3. If you're new to it, start with the [Ensuring
|
|
instant
|
|
navigations](https://preview.nextjs.org/docs/app/guides/instant-navigation)
|
|
guide for an overview of what instant navigations are and how Next.js
|
|
validates them, then come back here for the specific fix.
|
|
</div>
|
|
|
|
During [prerendering](https://preview.nextjs.org/docs/app/glossary#prerendering), [`generateViewport()`](https://preview.nextjs.org/docs/app/api-reference/functions/generate-viewport) read a per-request value ([`cookies()`](https://preview.nextjs.org/docs/app/api-reference/functions/cookies), [`headers()`](https://preview.nextjs.org/docs/app/api-reference/functions/headers), [`params`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/page#params-optional), [`searchParams`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional)). With [Cache Components](https://preview.nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) enabled, viewport metadata can't be deferred behind a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary because it affects the initial page load. The page can't be prerendered, so navigations block instead of being [instant](https://preview.nextjs.org/docs/app/guides/instant-navigation).
|
|
|
|
Uncached data accesses ([`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch), database calls, [`await connection()`](https://preview.nextjs.org/docs/app/api-reference/functions/connection)) in `generateViewport()` have different fixes. See [Next.js encountered uncached data in `generateViewport()`](/docs/messages/blocking-prerender-viewport-dynamic). The metadata equivalent is handled at [Runtime data in `generateMetadata()`](/docs/messages/blocking-prerender-metadata-runtime). For errors in the page body rather than viewport, see [Next.js encountered runtime data during prerendering](/docs/messages/blocking-prerender-runtime).
|
|
|
|
## Ways to fix this
|
|
|
|
<FixCardGrid>
|
|
<FixCard
|
|
group="static"
|
|
title="Use static viewport"
|
|
href="#use-static-viewport"
|
|
snippets={[
|
|
{ text: 'export const viewport = {', highlight: true },
|
|
{ text: ' themeColor: "#000"' },
|
|
{ text: '}' },
|
|
]}
|
|
/>
|
|
<FixCard
|
|
group="block"
|
|
title="Allow blocking route"
|
|
href="#allow-blocking-route"
|
|
snippets={[
|
|
{ text: '// page.tsx or layout.tsx' },
|
|
{ text: 'export const instant = false', highlight: true },
|
|
]}
|
|
/>
|
|
</FixCardGrid>
|
|
|
|
## 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()`](https://preview.nextjs.org/docs/app/api-reference/functions/generate-viewport) with a static [`viewport`](https://preview.nextjs.org/docs/app/api-reference/functions/generate-viewport#the-viewport-object) export, or rewrite `generateViewport()` so it no longer reads [`cookies()`](https://preview.nextjs.org/docs/app/api-reference/functions/cookies), [`headers()`](https://preview.nextjs.org/docs/app/api-reference/functions/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.
|
|
|
|
```jsx filename="app/layout.js"
|
|
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](https://preview.nextjs.org/docs/app/api-reference/functions/generate-viewport#the-viewport-object).
|
|
|
|
### 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](#allow-blocking-route).
|
|
|
|
### Gotchas
|
|
|
|
- The `viewport` export is typically set in the root layout. Changing a layout's viewport affects every route in its subtree.
|
|
- [`themeColor`](https://preview.nextjs.org/docs/app/api-reference/functions/generate-viewport#themecolor) is the most common reason for a dynamic `generateViewport`. 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`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/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>`](https://react.dev/reference/react/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`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/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.
|
|
|
|
```jsx filename="app/dashboard/layout.js"
|
|
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](https://preview.nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant).
|
|
|
|
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](#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 [`instant`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant) to `false` opts only the segment that exports it out. Descendant segments are still validated by the global default.
|
|
- This export does not disable [prerendering](https://preview.nextjs.org/docs/app/glossary#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 `themeColor` with 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's `generateViewport` and must be statically prerendered. [`instant = false`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant) 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, [Use static viewport](#use-static-viewport) instead, or move to [`global-not-found.js`](/docs/app/api-reference/file-conventions/not-found#global-not-foundjs-experimental), which bypasses the root layout entirely and avoids inheriting its `generateViewport`.
|
|
|
|
## 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>`](https://react.dev/reference/react/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`](https://preview.nextjs.org/docs/app/api-reference/cli/next#next-dev-options), the error overlay points at the failing component with file paths and line numbers. When working from a build instead, the default [`next build`](https://preview.nextjs.org/docs/app/api-reference/cli/next#next-build-options) 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](https://preview.nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) apps and is what surfaces this error.
|
|
|
|
- **One segment**: add [`export const instant = false`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant) to the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations.
|
|
- **Entire app**: set [`experimental.instantInsights.validationLevel`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#configuring-validation-defaults) to `'manual-warning'` in `next.config`. This limits validation to segments that explicitly export `instant`.
|
|
|
|
See [Ensuring instant navigations](https://preview.nextjs.org/docs/app/guides/instant-navigation) for the full model.
|
|
|
|
## Related Insights
|
|
|
|
- [Runtime data during prerendering](/docs/messages/blocking-prerender-runtime)
|
|
- [Uncached data during prerendering](/docs/messages/blocking-prerender-dynamic)
|
|
- [URL data in a Client Component outside of Suspense](/docs/messages/blocking-prerender-client-hook)
|
|
- [Runtime data in `generateMetadata()`](/docs/messages/blocking-prerender-metadata-runtime)
|
|
- [Uncached data in `generateMetadata()`](/docs/messages/blocking-prerender-metadata-dynamic)
|
|
- [Uncached data in `generateViewport()`](/docs/messages/blocking-prerender-viewport-dynamic)
|
|
- [`Math.random()` while prerendering](/docs/messages/blocking-prerender-random)
|
|
- [`Math.random()` in a Client Component](/docs/messages/blocking-prerender-random-client)
|
|
- [`Date.now()` while prerendering](/docs/messages/blocking-prerender-current-time)
|
|
- [`Date.now()` in a Client Component](/docs/messages/blocking-prerender-current-time-client)
|
|
- [Crypto APIs while prerendering](/docs/messages/blocking-prerender-crypto)
|
|
- [Crypto APIs in a Client Component](/docs/messages/blocking-prerender-crypto-client)
|
|
- [Dynamic data during prefetching](/docs/messages/instant-link-prefetch-partial)
|
|
- [URL data outside of Suspense](/docs/messages/instant-shell-url-data)
|
|
- [Unrendered segment](/docs/messages/instant-unrendered-segment)
|