| | |

Next.js 20 ▲ ensureStatic — Enforcing static shells & preventing dynamic leakage

⏱️ Estimated read: 11 min | |

ensureStatic is a route segment config introduced in Next.js 16.4 that turns a static requirement into a build-time guarantee. Without it, a route can be static today and become dynamic tomorrow because someone added a cookies() call in a child component. With it, the build fails if the route’s chosen navigation stage cannot be prerendered. The export accepts five values—'auto', 'shell', 'prefetch', 'navigation', and false—and each level covers a different portion of the server-rendered output that must stay static .

This chapter covers the three strictness levels, the Cache Components and Partial Prefetching prerequisites, the validation behavior that distinguishes 'navigation' from the other levels, and the false escape hatch that prevents a parent segment from imposing a static requirement on a subtree that cannot satisfy it.

Key point: ensureStatic = 'navigation' is the only level that validates during build and development. The 'shell' and 'prefetch' levels shape the prefetch stages but do not add build validation; they rely on Instant Navigation validation to report request-specific work that blocks a navigation .


Why ensureStatic exists

The leakage problem. A route that is static at one point in time can become dynamic without anyone noticing. A developer adds a cookies() call to a shared component, or a searchParams read to a layout, and the route silently switches to request-time rendering. The performance regression is invisible until a user reports a slow navigation or a CDN cache miss. ensureStatic makes the requirement explicit and enforceable: the build fails when the route cannot satisfy it .

The prefetch cost problem. With Partial Prefetching, a visible <Link prefetch={true}> can trigger a server render for each link. A grid of twenty product links can produce twenty request-time renders before the user clicks anything. The 'prefetch' level requires the per-link prefetch to be served from static output, which eliminates those runtime renders . The 'shell' level only requires the App Shell to be static; a prefetch={true} link into a route that reads cookies() can still trigger a runtime per-link prefetch .

The composition problem. ensureStatic values compose across layouts and pages. A parent layout that sets 'navigation' imposes the requirement on every route beneath it. A child segment cannot relax the requirement; it can only add a stricter one or set false to block a parent’s requirement from applying to its subtree . This is why the value must be chosen with the route hierarchy in mind.


a. The three strictness levels

The levels are cumulative. Each stricter level includes the stages of the levels before it.

'shell' requires the App Shell loaded by a default <Link> to be static. The shell can include static content, cached content, and <Suspense> fallbacks. Request-specific content behind <Suspense> can render during a later per-link prefetch or during navigation. Without Partial Prefetching, there is no App Shell, so the value has no effect and Next.js logs a warning .

'prefetch' requires both the App Shell and the per-link prefetches to be static. A prefetch={true} link resolves URL data before the click, and the cached content that depends on that URL data must be part of the static output. Without Partial Prefetching, the value adds no requirement and links keep their normal full-prefetch behavior . The build does not validate this level; it is a shaping tool for the prefetch stage, not a build-time guarantee .

'navigation' requires the complete server-rendered route to be produced during prerendering. Initial loads and client navigations both use the static output. This is the only level that works with Cache Components alone (without Partial Prefetching), and the only level that validates during next dev and next build . If the route reads cookies(), headers(), params, or searchParams during prerendering, the build fails with a source-mapped error that points at the failing component .


b. The prerequisites and validation

Every level requires cacheComponents: true in next.config.ts. The 'shell' and 'prefetch' levels additionally require partialPrefetching: true. Exporting ensureStatic from a Client Component throws an error .

The validation differs by level. 'navigation' is validated during both next dev and next build. The development overlay points at the failing component with file paths and line numbers, and the error includes a fix card for the specific data access: remove the read, cache it with 'use cache', or move it to the client . The build output is more abbreviated, but next build --debug-prerender provides full user-frame stack traces .

The 'shell' and 'prefetch' levels do not add build validation. When Instant Navigation validation is enabled, it can report request-specific work outside <Suspense> because that work blocks the navigation, but the report is an insight, not a build failure . The distinction matters: 'navigation' is a hard guarantee; 'shell' and 'prefetch' are shaping tools that rely on the developer to fix the navigations they expose.

A route that exports 'navigation' and uses a dynamic route segment must also export generateStaticParams() with at least one value. Without it, the build fails with an error that names the missing function .


c. The false escape hatch

false does not select a static level. It preserves normal rendering behavior and prevents another segment from applying 'shell', 'prefetch', or 'navigation' to the route. This is the mechanism for a subtree that cannot satisfy a parent’s static requirement, such as a route that must read a per-visitor consent cookie in its layout .

A root layout that sets 'navigation' imposes the requirement on every route. A route group that needs request-time rendering can set ensureStatic = false in its own layout to opt out. The opt-out is scoped to that segment and its children; the rest of the application keeps the parent’s requirement.


Complete Example Session

// ============================================
// PART 1: ENABLE CACHE COMPONENTS
// ============================================
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
}

export default nextConfig

// ensureStatic requires cacheComponents.
// shell and prefetch require partialPrefetching.
// ============================================
// PART 2: THE SHELL LEVEL
// ============================================
// app/store/layout.tsx
export const ensureStatic = 'shell'

export default function StoreLayout({ children }) {
  return <div>{children}</div>
}

// The App Shell must be static.
// Per-link prefetches can still render per request.
// ============================================
// PART 3: THE PREFETCH LEVEL
// ============================================
// app/products/[slug]/page.tsx
export const ensureStatic = 'prefetch'

export default async function ProductPage({ params }) {
  const { slug } = await params
  return <ProductDetails slug={slug} />
}

// The App Shell and per-link prefetches must be static.
// A prefetch={true} link cannot trigger a server render.
// ============================================
// PART 4: THE NAVIGATION LEVEL
// ============================================
// app/guides/[id]/page.tsx
export const ensureStatic = 'navigation'

export async function generateStaticParams() {
  return [{ id: 'coffee' }, { id: 'tea' }]
}

export default async function GuidePage({ params }) {
  const { id } = await params
  return <Guide id={id} />
}

// The entire route must be static.
// generateStaticParams is required for dynamic segments.
// ============================================
// PART 5: THE CACHED DATA
// ============================================
// app/guides/[id]/page.tsx
import { cacheLife } from 'next/cache'

async function getGuide(id: string) {
  'use cache'
  cacheLife('hours')
  return db.guide.findUnique({ where: { id } })
}

// Uncached data fails the navigation requirement.
// 'use cache' makes it static.
// ============================================
// PART 6: THE RUNTIME DATA FAILURE
// ============================================
// app/guides/[id]/page.tsx
export default async function GuidePage({ params }) {
  const { id } = await params
  const theme = (await cookies()).get('theme')
  // Build fails: runtime data on a route that must be fully static.
}
// ============================================
// PART 7: THE FIX — CLIENT READ
// ============================================
// app/guides/theme.tsx
'use client'
import { use } from 'react'
import { browser } from 'react-dom'

export function Theme() {
  use(browser('Theme is stored in a cookie.'))
  const match = document.cookie.match(/theme=(\w+)/)
  return <p>Theme: {match?.[1] ?? 'light'}</p>
}

// The browser reads the cookie after the static shell loads.
// ============================================
// PART 8: THE FALSE OPT-OUT
// ============================================
// app/admin/layout.tsx
export const ensureStatic = false

export default function AdminLayout({ children }) {
  return <div>{children}</div>
}

// The admin subtree opts out of a parent's requirement.
// ============================================
// PART 9: THE BUILD VERIFICATION
// ============================================
// next build

// A navigation-level failure names the route and the fix.
// The build succeeds only when every requirement is met.
// ============================================
// PART 10: THE DECISION TREE
// ============================================
// Does the entire route need to be static?
//   Yes → 'navigation'
//
// Does the per-link prefetch need to be static?
//   Yes → 'prefetch'
//
// Does only the App Shell need to be static?
//   Yes → 'shell'
//
// Must the subtree opt out of a parent's requirement?
//   Yes → false

The ten parts covered the config, the shell level, the prefetch level, the navigation level, the cached data, the runtime data failure, the client read fix, the false opt-out, the build verification, and the decision tree.


Quick Reference

The ensureStatic values

ValueStatic requirementBuild validation
'auto'None (default)None
'shell'App ShellNone (insight only)
'prefetch'App Shell + per-link prefetchesNone (insight only)
'navigation'Complete routeYes (next dev and next build)
falseOpts out of parent requirementNone

Prerequisites

RequirementDetail
cacheComponents: trueRequired for all levels
partialPrefetching: trueRequired for 'shell' and 'prefetch'
Server ComponentExporting from a Client Component throws
generateStaticParams()Required for dynamic segments with 'navigation'

Validation errors

ErrorCauseFix
Runtime data on a static routecookies(), headers(), params, searchParamsRemove, cache, or move to client
Uncached data on a static routeUncached fetch, database call, connection()Add 'use cache' or remove
Missing generateStaticParamsDynamic segment with 'navigation'Export the function with values
Short cache lifetimeexpire under 5m, stale under 30s, revalidate: 0Extend the cache lifetime

Composition

Parent valueChild valueResult
'navigation''shell'Parent’s 'navigation' applies
'navigation'falseChild subtree opts out
'shell''prefetch'Child is stricter

Best Practices

✅ Do This:

// Use 'navigation' for routes that must always be static
export const ensureStatic = 'navigation'                     // ✅
// Add generateStaticParams for dynamic segments
export async function generateStaticParams() { ... }         // ✅
// Cache uncached data with 'use cache'
async function getData() { 'use cache'; ... }                // ✅
// Use false to opt a subtree out of a parent's requirement
export const ensureStatic = false                            // ✅

❌ Don’t Do This:

// Export ensureStatic from a Client Component
'use client'
export const ensureStatic = 'navigation'                     // ❌
// Use 'shell' and expect prefetch={true} to be static
// 'shell' only covers the App Shell                       // ❌
// Set 'navigation' without generateStaticParams on a dynamic route
// The build fails                                           // ❌
// Use 'navigation' on a route that must read cookies
// The build fails; use false or a client read               // ❌

Common Pitfalls

PitfallWhy It HappensFix
Build fails on runtime datacookies() or headers() in the routeRemove the read, cache it, or move to client
Build fails on uncached dataUncached fetch or database callAdd 'use cache' with cacheLife
generateStaticParams errorDynamic segment with 'navigation'Export the function with at least one value
ensureStatic is a no-opcacheComponents not enabledEnable the flag
'shell' does not stop runtime prefetchesprefetch={true} link reads request dataUse 'prefetch' instead
Parent’s requirement applies unexpectedlyensureStatic composes across segmentsSet false in the child segment

Real-World Examples

1. Navigation level

export const ensureStatic = 'navigation'

2. Prefetch level

export const ensureStatic = 'prefetch'

3. Shell level

export const ensureStatic = 'shell'

4. Opt-out

export const ensureStatic = false

5. Cached data

'use cache'
cacheLife('hours')

6. generateStaticParams

export async function generateStaticParams() { return [{ id: '1' }] }

7. Client read

'use client'
use(browser('...'))

8. The config

cacheComponents: true, partialPrefetching: true

9. Build debug

next build --debug-prerender

10. The decision

Whole route static? → navigation
Prefetch static? → prefetch
Shell static? → shell
Opt out? → false

Visual

The three levels

┌─────────────────────────────────────────────────────────────┐
│  'shell'                                                     │
│    └── App Shell                                             │
│                                                              │
│  'prefetch'                                                  │
│    ├── App Shell                                             │
│    └── per-link prefetch                                     │
│                                                              │
│  'navigation'                                                │
│    ├── App Shell                                             │
│    ├── per-link prefetch                                     │
│    └── complete route                                        │
└─────────────────────────────────────────────────────────────┘

The validation

┌─────────────────────────────────────────────────────────────┐
│  'navigation'                                                │
│    → next build validates                                    │
│    → next dev overlay with source maps                       │
│    → error names the fix                                     │
│                                                              │
│  'shell' / 'prefetch'                                        │
│    → no build validation                                     │
│    → Instant Insight reports blocking work                   │
└─────────────────────────────────────────────────────────────┘

The composition

┌─────────────────────────────────────────────────────────────┐
│  Root layout: ensureStatic = 'navigation'                    │
│       │                                                      │
│       ├── /blog ──► navigation applies                       │
│       │                                                      │
│       └── /admin ──► ensureStatic = false                    │
│                        → opts out                            │
└─────────────────────────────────────────────────────────────┘

The false opt-out

┌─────────────────────────────────────────────────────────────┐
│  Parent sets 'navigation'.                                   │
│  Child needs request-time rendering.                         │
│                                                              │
│  Child: export const ensureStatic = false                    │
│    → parent's requirement does not apply to this subtree     │
│    → the rest of the app keeps the requirement               │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Introduced inNext.js 16.4
Values'auto', 'shell', 'prefetch', 'navigation', false
Default'auto'
PrerequisitecacheComponents: true
'shell'/'prefetch' prerequisitepartialPrefetching: true
Build validation'navigation' only
Dynamic segment requirementgenerateStaticParams() with 'navigation'
Opt-outfalse
Client Component exportError

Key takeaways:

  • ensureStatic turns a static requirement into a build-time guarantee. Without it, a route can become dynamic silently. With it, the build fails when the route cannot satisfy the chosen level .
  • The three levels are cumulative and have different validation. 'shell' covers the App Shell, 'prefetch' adds per-link prefetches, and 'navigation' covers the complete route. Only 'navigation' validates during build and development; the other two shape the prefetch stages and rely on Instant Navigation insights .
  • 'navigation' requires generateStaticParams() on dynamic segments. Without it, the build fails with an error that names the missing function .
  • Uncached data and runtime data fail the navigation requirement. The fixes are to remove the read, cache it with 'use cache', or move it to a client component. Wrapping the read in <Suspense> or setting instant = false does not relax the requirement .
  • false opts a subtree out of a parent’s requirement. It is the mechanism for a route group that needs request-time rendering while the rest of the application stays static .
  • The value composes across layouts and pages. A parent’s requirement applies to every route beneath it unless a child sets false. The stricter value wins when a parent and child both set a level .

Remember: ensureStatic is the enforcement mechanism for the Instant Navigations discipline. Stream and Cache make a route instant; ensureStatic ensures it stays that way. Use 'navigation' when the entire route must be static, 'prefetch' when the per-link prefetches must not trigger server renders, and 'shell' when only the App Shell must be static. Use false only for a subtree that genuinely cannot satisfy a parent’s requirement. The build validation is the point: a static route that can silently become dynamic is a performance regression waiting to happen.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!