Files
next.js/errors/blocking-prerender-random.mdx
Aurora ScharffandGitHub 76628b4f9b [PP] Surface URL data during prefetching as an Instant insight with rule page (#95365)
## 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 -->
2026-07-08 04:04:01 +02:00

255 lines
16 KiB
Plaintext

---
title: Next.js encountered the unstable value Math.random() while prerendering
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), a Server Component called [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) outside of [`<Suspense>`](https://react.dev/reference/react/Suspense). With [Cache Components](https://preview.nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) enabled, Next.js can't bake an unpredictable value into the prerendered HTML. The value at build time will differ from the value at runtime, so you need to choose: cache the value so it's stable, defer the call behind a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary so it runs per-request, or move it to the client.
Other unpredictable APIs ([`Date.now()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now), [`crypto.randomUUID()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID)) have parallel error pages: see [`Date.now()`](/docs/messages/blocking-prerender-current-time) and [crypto APIs](/docs/messages/blocking-prerender-crypto). The Client Component case is handled at [`Math.random()` in a Client Component](/docs/messages/blocking-prerender-random-client).
## Ways to fix this
<FixCardGrid>
<FixCard
group="dynamic"
title="Generate on every request"
href="#generate-on-every-request"
snippets={[
{ text: 'await connection()', highlight: true },
{ text: 'const id = Math.random()' },
{ text: 'return <Item id={id} />' },
]}
/>
<FixCard
group="cache"
title="Cache the random value"
href="#cache-the-random-value"
snippets={[
{ text: 'function RandomId() {' },
{ text: ' "use cache"', highlight: true },
{ text: ' return String(Math.random())' },
]}
/>
<FixCard
group="client"
title="Render on the client"
href="#render-on-the-client"
snippets={[
{ text: '"use client"', highlight: true },
{ text: '// runs in the browser' },
{ text: 'const id = Math.random()' },
]}
/>
</FixCardGrid>
## Generate on every request
Choose this fix when each request genuinely needs a different value. A [unique session ID](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy), a single-use nonce, an A/B test bucket: anything that has to be fresh per visitor. Add [`await connection()`](https://preview.nextjs.org/docs/app/api-reference/functions/connection) before the call to tell Next.js the surrounding component is request-bound. The component is excluded from the prerender and streamed in from the nearest [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary on each request.
### Patterns
#### Use `await connection()` before the random call
Call [`connection()`](https://preview.nextjs.org/docs/app/api-reference/functions/connection) before [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random). Everything after the `await` is request-time. Wrap the component in [`<Suspense>`](https://react.dev/reference/react/Suspense) so the surrounding shell stays prerendered and only the dynamic part streams in.
Push the [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary as close to the random read as possible. If the parent has cached content (a header, stats, navigation), isolate the random read in its own component so only that piece falls behind the boundary.
```jsx filename="app/dashboard/page.js"
import { Suspense } from 'react'
export default function Page() {
return (
<DashboardShell>
<Suspense fallback={<TraceSkeleton />}>
<RequestTrace />
</Suspense>
<CachedStats />
</DashboardShell>
)
}
```
```jsx filename="app/dashboard/request-trace.js"
import { connection } from 'next/server'
export async function RequestTrace() {
await connection()
const traceId = Math.random().toString(16).slice(2)
return <small>trace: {traceId}</small>
}
```
#### Alternative: `await io()`
Use [`io()`](https://preview.nextjs.org/docs/app/api-reference/functions/io) from `next/cache` to keep the read out of the static shell. Unlike [`connection()`](https://preview.nextjs.org/docs/app/api-reference/functions/connection), `io()` doesn't block prefetches and works inside `"use cache"` scopes and Client Components.
```jsx filename="app/dashboard/request-trace.js"
import { io } from 'next/cache'
export async function RequestTrace() {
await io()
const traceId = Math.random().toString(16).slice(2)
return <small>trace: {traceId}</small>
}
```
Learn more: [`connection`](https://preview.nextjs.org/docs/app/api-reference/functions/connection), [`io`](https://preview.nextjs.org/docs/app/api-reference/functions/io), [Streaming](https://preview.nextjs.org/docs/app/guides/streaming).
### Trade-off
The route renders on every request. The shell still ships instantly because of the [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary, but the dynamic region waits on the server render before it can paint. Make sure the fallback approximates the final layout so it doesn't cause a layout shift when the value arrives. See [minimizing layout shift](https://preview.nextjs.org/docs/app/guides/streaming#cls-cumulative-layout-shift).
### Gotchas
- Any UI rendered as part of the prerender shell must be deterministic. That includes [`<Suspense>`](https://react.dev/reference/react/Suspense) fallbacks, [`loading.js`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/loading), [`error.js`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/error), [`not-found.js`](/docs/app/api-reference/file-conventions/not-found), and [`global-error.js`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/error#global-error). Calling [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) or [`Date.now()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) in any of them raises this same error.
- If [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) is being used as a unique ID for logging or correlation, consider an incrementing integer or [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) request scope. Those don't trigger the error at all because they aren't unpredictable from Next.js's point of view.
- Random values produced inside third-party packages will surface this error in your project code. The same fixes apply at the call site that consumes the value.
## Cache the random value
Choose this fix when one stable random value per build, deployment, or `cacheLife` window is acceptable. The classic case is a daily shuffle of items where the same shuffle is fine for every visitor that day. Move the [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) call into a function with [`use cache`](https://preview.nextjs.org/docs/app/api-reference/directives/use-cache) as the first statement. Next.js evaluates the function once per cache key and reuses the result.
### Patterns
#### Cache the producer function
Wrap the random generation in its own function with [`use cache`](https://preview.nextjs.org/docs/app/api-reference/directives/use-cache). The returned value is part of the cache entry, so every consumer sees the same random number until the cache is invalidated.
```jsx filename="app/page.js"
async function getRandomSeed() {
'use cache'
return Math.random()
}
export default async function Page() {
const products = await getCachedProducts()
const seed = await getRandomSeed()
return <ProductsView products={randomize(products, seed)} />
}
```
Learn more: [Caching with `use cache`](https://preview.nextjs.org/docs/app/api-reference/directives/use-cache).
#### Control the rotation window with `cacheLife`
When you want the random value to rotate on a schedule (a daily featured item, an hourly shuffle), set a [`cacheLife`](https://preview.nextjs.org/docs/app/api-reference/functions/cacheLife) profile.
```jsx filename="app/page.js"
import { cacheLife } from 'next/cache'
async function getDailySeed() {
'use cache'
cacheLife('days')
return Math.random()
}
```
Learn more: [How to configure cache lifetimes](https://preview.nextjs.org/docs/app/api-reference/functions/cacheLife).
### Trade-off
Every visitor in the cache window sees the same "random" value. That's the right answer for global ordering and feature rotation, but the wrong answer for per-user uniqueness or anything security-sensitive (session IDs, CSRF tokens, nonces). For unique-per-request values use [Generate on every request](#generate-on-every-request).
### Gotchas
- Inside a [`use cache`](https://preview.nextjs.org/docs/app/api-reference/directives/use-cache) scope, you can't call [`cookies()`](https://preview.nextjs.org/docs/app/api-reference/functions/cookies) or [`headers()`](https://preview.nextjs.org/docs/app/api-reference/functions/headers), which means you can't key the random value by request identity.
- If the same random value is used in many places, hoist the cached function up so all consumers share the cache entry instead of producing a different cached value at each call site.
- If you cache a function and still see this error, the [`cacheLife`](https://preview.nextjs.org/docs/app/api-reference/functions/cacheLife) may be too short to prerender. See [Short-lived caches](#short-lived-caches).
### Short-lived caches
[`use cache`](https://preview.nextjs.org/docs/app/api-reference/directives/use-cache) accepts a [`cacheLife`](https://preview.nextjs.org/docs/app/api-reference/functions/cacheLife) profile. A short profile (such as `"seconds"` or `"minutes"`) whose `revalidate` is shorter than the prerender's effective lifetime prevents the value from being included in the prerender. The segment becomes a dynamic hole instead. The cache entry still helps the [Client Cache](https://preview.nextjs.org/docs/app/glossary#client-cache) and protects upstream APIs, but the page falls back to streaming.
To keep the page prerendered, use a profile with a longer revalidate window such as `"default"` (15 minutes), `"hours"`, or `"days"`. If a short profile is intentional, treat the value as dynamic and use [Generate on every request](#generate-on-every-request) instead.
## Render on the client
Choose this fix when the random value belongs to the client experience. A canvas seed for a confetti animation, a random color for an avatar placeholder, a UI nonce that only matters in the browser. Move the component into a [Client Component](/docs/app/getting-started/server-and-client-components) so the value is produced after hydration, not during prerender.
### Patterns
#### Compute the value inside `useEffect`
Add the [`use client`](/docs/app/api-reference/directives/use-client) directive. Initialize state to a deterministic placeholder and assign the real value inside [`useEffect`](https://react.dev/reference/react/useEffect), which runs only in the browser after hydration.
```jsx filename="app/avatar.js"
'use client'
import { startTransition, useEffect, useState } from 'react'
export function Avatar() {
const [color, setColor] = useState('#888')
useEffect(() => {
// Wrap in startTransition so that if any component below suspends
// during this update, React keeps the existing UI visible instead
// of flashing the nearest outer <Suspense> fallback.
startTransition(() => {
setColor(`#${Math.random().toString(16).slice(2, 8)}`)
})
}, [])
return <div style={{ background: color }} />
}
```
#### Inline render with a parent `<Suspense>` boundary
If the random value needs to be part of the server-rendered HTML (not deferred to after hydration), the component can call [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) during render as long as a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary wraps it from the parent. Next.js prerenders the fallback and fills in the real component at request time. See [`Math.random()` in a Client Component](/docs/messages/blocking-prerender-random-client) for the full recipe.
Learn more: [Client Components](/docs/app/getting-started/server-and-client-components#using-client-components), [`Math.random()` in a Client Component](/docs/messages/blocking-prerender-random-client).
### Trade-off
The first paint shows the SSR fallback or initial state, and the random value appears only after the browser hydrates the component. That's fine for UI flourishes but wrong for content that has to be in the prerendered HTML. See [Preventing flash before hydration](https://preview.nextjs.org/docs/app/guides/preventing-flash-before-hydration) for techniques that eliminate the flash.
### Gotchas
- A Client Component that produces a random value inline during render still trips this error during SSR. See the dedicated [`Math.random()` in a Client Component](/docs/messages/blocking-prerender-random-client) page for the [`<Suspense>`](https://react.dev/reference/react/Suspense) and effect-based recipes.
## 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.
## Why `instant = false` doesn't clear this error
This error fires from the prerender, not from instant-navigation validation. `Math.random()` returns a different value on every render, so the prerender can't bake it into a static shell regardless of the segment's `instant` config or [`experimental.instantInsights.validationLevel`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#configuring-validation-defaults). Use one of the fixes above.
## 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)
- [Runtime data in `generateViewport()`](/docs/messages/blocking-prerender-viewport-runtime)
- [Uncached data in `generateViewport()`](/docs/messages/blocking-prerender-viewport-dynamic)
- [`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)