Next.js 20 ▲ ensureStatic — Enforcing static shells & preventing dynamic leakage
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
| Value | Static requirement | Build validation |
|---|---|---|
'auto' | None (default) | None |
'shell' | App Shell | None (insight only) |
'prefetch' | App Shell + per-link prefetches | None (insight only) |
'navigation' | Complete route | Yes (next dev and next build) |
false | Opts out of parent requirement | None |
Prerequisites
| Requirement | Detail |
|---|---|
cacheComponents: true | Required for all levels |
partialPrefetching: true | Required for 'shell' and 'prefetch' |
| Server Component | Exporting from a Client Component throws |
generateStaticParams() | Required for dynamic segments with 'navigation' |
Validation errors
| Error | Cause | Fix |
|---|---|---|
| Runtime data on a static route | cookies(), headers(), params, searchParams | Remove, cache, or move to client |
| Uncached data on a static route | Uncached fetch, database call, connection() | Add 'use cache' or remove |
Missing generateStaticParams | Dynamic segment with 'navigation' | Export the function with values |
| Short cache lifetime | expire under 5m, stale under 30s, revalidate: 0 | Extend the cache lifetime |
Composition
| Parent value | Child value | Result |
|---|---|---|
'navigation' | 'shell' | Parent’s 'navigation' applies |
'navigation' | false | Child 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Build fails on runtime data | cookies() or headers() in the route | Remove the read, cache it, or move to client |
| Build fails on uncached data | Uncached fetch or database call | Add 'use cache' with cacheLife |
generateStaticParams error | Dynamic segment with 'navigation' | Export the function with at least one value |
ensureStatic is a no-op | cacheComponents not enabled | Enable the flag |
'shell' does not stop runtime prefetches | prefetch={true} link reads request data | Use 'prefetch' instead |
| Parent’s requirement applies unexpectedly | ensureStatic composes across segments | Set 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
| Item | Value |
|---|---|
| Introduced in | Next.js 16.4 |
| Values | 'auto', 'shell', 'prefetch', 'navigation', false |
| Default | 'auto' |
| Prerequisite | cacheComponents: true |
'shell'/'prefetch' prerequisite | partialPrefetching: true |
| Build validation | 'navigation' only |
| Dynamic segment requirement | generateStaticParams() with 'navigation' |
| Opt-out | false |
| Client Component export | Error |
Key takeaways:
ensureStaticturns 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'requiresgenerateStaticParams()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 settinginstant = falsedoes not relax the requirement . falseopts 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.
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!