Next.js 21 ▲ Navigation API — Excluding prefetched content with await navigation()
The navigation() function is a server-side API that controls which parts of a route are included in a prefetch. It is part of the Instant Navigations feature introduced in Next.js 16.3, and it exists to solve a specific cost problem: a prefetch that renders an entire page, including its most expensive data, does work for a link the user may never click. await navigation() marks the boundary. Everything above it can render in the App Shell and in per-link prefetches. Everything below it waits for an actual navigation .
This chapter covers the navigation() function, its distinction from prefetch() and connection(), the Cache Components and Partial Prefetching prerequisites, the Suspense boundary requirement, and the restriction that prevents it from being called inside a 'use cache' scope.
Key point: await navigation() excludes the subtree below it from runtime prefetches, but it does not make that work request-dependent. A function called after the boundary can still use 'use cache' and keep its cache lifetime. The API requires cacheComponents: true and a <Suspense> boundary above the component that calls it.
Why navigation() exists
The prefetch cost problem. A default <Link> loads the App Shell of the destination route. A <Link prefetch={true}> goes further: it renders content that depends on the link’s URL, which means the server does work for every visible link with that attribute. If the page contains an expensive ranking, a full comment thread, or a personalized dashboard widget, the prefetch pays for that work even if the user never clicks .
The request-dependency problem. The connection() function is the existing mechanism for excluding work from prerendering, but it makes the work request-dependent. A cached function cannot be wrapped in connection() because the cache directive and the request dependency conflict. The navigation() function fills the gap: it excludes the work from prefetches without making it request-dependent, so the work can remain cacheable .
The stage problem. Next.js loads server-rendered output in stages. The App Shell is the first stage, the per-link prefetch is the second, and the navigation is the third. navigation() is the API that controls which content reaches the third stage only. The prefetch() function is the companion API that excludes content from the first stage but allows it in the second .
a. The prerequisites and the Suspense boundary
The function requires Cache Components to be enabled in next.config.ts. Without the flag, it throws an error. It is designed for routes that use Partial Prefetching, which can be enabled globally or per route .
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
export default nextConfig
The function must be called inside an async Server Component, and that component must be wrapped in a <Suspense> boundary. The boundary must be above the component that calls navigation(). Placing a boundary inside the component does not catch the suspension, and the development overlay reports the API call outside <Suspense> as an Instant Insight .
import { Suspense } from 'react'
import { navigation } from 'next/cache'
async function ExpensiveContent() {
await navigation() // everything below renders on navigation
const data = await fetchExpensiveData()
return <div>{data}</div>
}
export default function Page() {
return (
<main>
<h1>Header (prefetched)</h1>
<Suspense fallback={<div>Loading…</div>}>
<ExpensiveContent />
</Suspense>
</main>
)
}
The <h1> is part of the App Shell. The ExpensiveContent component is excluded from the shell and from per-link prefetches. The <Suspense> fallback is what the prefetched output contains, and the resolved content streams in when the user navigates .
b. The distinction from prefetch() and connection()
The three APIs are easy to confuse, and the distinction determines which one a route should use.
navigation() excludes content from both the App Shell and per-link prefetches. The content renders only during an actual navigation. A function called after the boundary can use 'use cache' and keep its cache lifetime .
prefetch() excludes content from the App Shell but includes it in a <Link prefetch={true}> prefetch. It is the choice when the content is expensive enough to keep out of the shell but the team wants it available before the click for links that opt into the deeper prefetch .
connection() waits for a real request. It excludes the content from prerendering and prefetching and makes the work request-dependent. A cached function cannot be called after connection() because the request dependency and the cache directive conflict .
The route that needs a reusable, cacheable ranking should use navigation(). The route that needs a per-user dashboard that must never be cached should use connection(). The route that wants the content in a per-link prefetch but not the shell should use prefetch() .
c. The restriction inside ‘use cache’
Calling await navigation() inside a 'use cache' or 'use cache: private' scope is an error. The supported pattern is to await navigation() in an uncached wrapper and place the 'use cache' directive on an inner function below the boundary .
async function Comments({ params }: { params: Promise<{ slug: string }> }) {
await navigation() // boundary outside the cache scope
const { slug } = await params
const sort = (await cookies()).get('comment-sort')?.value ?? 'newest'
return <CommentList items={await getComments(slug, sort)} />
}
async function getComments(slug: string, sort: string) {
'use cache'
cacheLife('hours')
return db.comments.list({ slug, sort })
}
The Comments component is the uncached wrapper that awaits navigation(). The getComments function is the cached inner function. The request-specific values (params, cookies()) are read after the boundary and passed as arguments, which allows the cache to key on the slug and the sort order rather than on the visitor .
The reason for the restriction is that a cached scope must be deterministic in its arguments. The navigation() call is a stage marker that depends on the rendering stage, not on the arguments, and placing it inside the cache scope would make the cached entry depend on when it was rendered rather than on what it was given.
Complete Example Session
// ============================================
// PART 1: THE CONFIG
// ============================================
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
export default nextConfig
// ============================================
// PART 2: THE BOUNDARY
// ============================================
import { Suspense } from 'react'
import { navigation } from 'next/cache'
async function ExpensiveContent() {
await navigation()
const data = await fetchExpensiveData()
return <div>{data}</div>
}
// ============================================
// PART 3: THE SUSPENSE WRAPPER
// ============================================
export default function Page() {
return (
<main>
<h1>Prefetched header</h1>
<Suspense fallback={<Skeleton />}>
<ExpensiveContent />
</Suspense>
</main>
)
}
// ============================================
// PART 4: THE CACHED INNER FUNCTION
// ============================================
async function Comments({ params }) {
await navigation()
const { slug } = await params
const sort = (await cookies()).get('sort')?.value ?? 'newest'
return <CommentList items={await getComments(slug, sort)} />
}
async function getComments(slug, sort) {
'use cache'
cacheLife('hours')
return db.comments.list({ slug, sort })
}
// ============================================
// PART 5: THE WRONG PATTERN
// ============================================
async function Wrong() {
'use cache'
await navigation() // error: not allowed inside a cache scope
}
// ============================================
// PART 6: THE prefetch() COMPARISON
// ============================================
async function DeferredToPrefetch() {
await prefetch() // excluded from shell, included in prefetch={true}
return <Expensive />
}
// ============================================
// PART 7: THE connection() COMPARISON
// ============================================
async function RequestDependent() {
await connection() // waits for a real request
return <PerUserData />
}
// ============================================
// PART 8: THE ensureStatic REQUIREMENT
// ============================================
// If the route must be fully static, use ensureStatic
export const ensureStatic = 'navigation'
// ============================================
// PART 9: THE instant OPTOUT
// ============================================
// If the route has no meaningful UI before the boundary
export const instant = false
// ============================================
// PART 10: THE DECISION
// ============================================
// Reusable + expensive + navigation-only? → navigation()
// Expensive + available in per-link prefetch? → prefetch()
// Request-dependent? → connection()
The ten parts covered the config, the boundary, the Suspense wrapper, the cached inner function, the wrong pattern, the prefetch() comparison, the connection() comparison, the ensureStatic requirement, the instant opt-out, and the decision.
Quick Reference
The three stage APIs
| API | Excluded from shell | Excluded from per-link prefetch | Request-dependent |
|---|---|---|---|
navigation() | Yes | Yes | No |
prefetch() | Yes | No | No |
connection() | Yes | Yes | Yes |
Prerequisites
| Requirement | Detail |
|---|---|
| Cache Components | cacheComponents: true |
| Partial Prefetching | partialPrefetching: true |
| Component type | async Server Component |
| Boundary | <Suspense> above the component |
Restrictions
| Restriction | Reason |
|---|---|
Inside 'use cache' | Stage marker conflicts with cache determinism |
Outside <Suspense> | No fallback for the prefetched output |
In generateMetadata | Separate insight |
In generateViewport | Separate insight |
The decision
| Content | API |
|---|---|
| Expensive, reusable, wait for navigation | navigation() |
| Expensive, want it in per-link prefetch | prefetch() |
| Per-user, must run per request | connection() |
Visual
The three stages
┌─────────────────────────────────────────────────────────────┐
│ STAGE 1: App Shell (default <Link>) │
│ ├── Header │
│ ├── Layout │
│ └── <Suspense> fallback for deferred content │
│ │
│ STAGE 2: Per-link prefetch (<Link prefetch={true}>) │
│ ├── Shell content │
│ ├── URL-dependent cached content │
│ └── <Suspense> fallback for navigation() content │
│ │
│ STAGE 3: Navigation (click) │
│ ├── Shell content │
│ ├── URL-dependent content │
│ └── navigation() content │
└─────────────────────────────────────────────────────────────┘
navigation() vs prefetch()
┌─────────────────────────────────────────────────────────────┐
│ navigation() │
│ Shell: fallback │
│ Prefetch: fallback │
│ Navigation: content │
│ │
│ prefetch() │
│ Shell: fallback │
│ Prefetch: content │
│ Navigation: content │
└─────────────────────────────────────────────────────────────┘
The cache restriction
┌─────────────────────────────────────────────────────────────┐
│ async function Wrapper() { │
│ await navigation() ← allowed (uncached wrapper) │
│ return <Inner slug={slug} /> │
│ } │
│ │
│ async function Inner({ slug }) { │
│ 'use cache' ← allowed (inner function) │
│ return db.query(slug) │
│ } │
│ │
│ async function Wrong() { │
│ 'use cache' │
│ await navigation() ← error │
│ } │
└─────────────────────────────────────────────────────────────┘
The Suspense requirement
┌─────────────────────────────────────────────────────────────┐
│ <Suspense fallback={<Skeleton />}> │
│ <ExpensiveContent /> ← awaits navigation() │
│ </Suspense> │
│ │
│ The boundary is above the component. │
│ The fallback is what the prefetch contains. │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Function | await navigation() |
| Import | next/cache |
| Introduced | Next.js 16.3 (Instant Navigations) |
| Requires | cacheComponents: true, <Suspense> |
| Excludes | Content from shell and per-link prefetches |
| Does not | Make the work request-dependent |
| Cache restriction | Not allowed inside 'use cache' |
| Companion | prefetch() for per-link prefetch inclusion |
| Contrast | connection() for request-dependent work |
Key takeaways:
navigation()is the stage boundary for content that should wait for an actual navigation. It excludes the subtree below it from the App Shell and from per-link prefetches. The nearest<Suspense>fallback is what the prefetched output contains, and the resolved content streams in when the user clicks .- It does not make the work request-dependent. Unlike
connection(), a function called afterawait navigation()can still use'use cache'and keep its cache lifetime. This is the key distinction: the work is reusable, but it should not be generated for speculative prefetches . prefetch()is the companion for per-link prefetch inclusion. A route that wants the content in a<Link prefetch={true}>prefetch but not in the App Shell usesprefetch(). A route that wants the content to wait for the click usesnavigation().- The
'use cache'restriction requires an uncached wrapper. Awaitingnavigation()inside a cached scope is an error. The supported pattern is to await the boundary in an uncached component and place the cache directive on an inner function called below it . - The
<Suspense>boundary is mandatory. The API needs a fallback to render in the prefetched output. Placing the boundary inside the component that callsnavigation()does not work, and the development overlay reports the API call outside<Suspense>as an insight . - The route can opt out of instant-navigation validation. A segment that has no meaningful UI before the deferred content can export
instant = falseto exempt itself from the validation that requires a boundary. This is the escape hatch for the route that intentionally blocks until the server render completes .
Remember: The navigation() API is a cost-control mechanism, not a performance trick. It tells Next.js that the content below the boundary is expensive enough that generating it for every link prefetch is wasteful. The content still renders on navigation, and it can still be cached. The three APIs—navigation(), prefetch(), and connection()—form a progression from “wait for the click” to “available in a per-link prefetch” to “must run per request.” Choose the one that matches the cost and the freshness requirement of the content.
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!