Static vs dynamic rendering in Next.js 16 is decided per component, not per route. Static means work that finishes at build time and ships as prerendered HTML. Dynamic means work that waits for the request because the data is not known until then.
This article assumes Cache Components, enabled with cacheComponents: true in next.config.ts. Under that model data is dynamic by default, and you opt in to static output with the use cache directive.
Static and dynamic at a glance
The two modes answer one question: when does this component run?
| Aspect | Static | Dynamic |
|---|---|---|
| When it renders | Build time or background revalidation | Request time |
| Depends on | Data known before the request | Cookies, headers, params, search params |
| Served from | CDN or disk | The server, then streamed |
| Freshness | Fixed until revalidated | Fresh on every request |
A static component only needs inputs available at build time. A dynamic component needs something that only exists once a real visitor arrives.
What makes a component static
Components that use only predictable values prerender automatically. Module imports, synchronous file reads, and pure computations produce the same output every time, so Next.js runs them once at build time and bakes the result into the shell.
// app/about/page.tsx
import fs from 'node:fs'
export default function AboutPage() {
const content = fs.readFileSync('./about.txt', 'utf-8')
return <article>{content}</article>
}This page is fully static. It reads no request data, so next build renders it once, stores the HTML, and serves the same output to every visitor without running the component again.
What makes a component dynamic
A component becomes dynamic when it reads a request-time API or waits on uncached data. In Next.js 16 that read streams behind a Suspense boundary instead of forcing the whole route dynamic.
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
import { Suspense } from 'react'
async function ThemeBanner() {
const theme = (await cookies()).get('theme')?.value ?? 'light'
return <p>Your theme is {theme}</p>
}
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading theme...</p>}>
<ThemeBanner />
</Suspense>
</main>
)
}The heading is static and ships immediately. The theme banner depends on a cookie, so the fallback renders in the shell and the real banner streams in when the cookie is read. The whole page does not become dynamic just because one part reads a cookie.
One page, both modes
A single route mixes static, cached, and streamed content. Static markup and cached data join the prerendered shell, while request-specific data streams behind fallbacks. First, cache the shared posts:
// app/lib/posts.ts
import { cacheLife } from 'next/cache'
export async function getLatestPosts() {
'use cache'
cacheLife('hours')
const res = await fetch('https://api.example.com/posts')
return res.json()
}Then compose the page with one cached read and one streamed read. PersonalFeed is the cookie reading component from the previous section, moved into its own file so the page stays small.
// app/blog/page.tsx
import { Suspense } from 'react'
import { getLatestPosts } from '../lib/posts'
import { PersonalFeed } from './personal-feed'
export default async function BlogPage() {
const posts = await getLatestPosts()
return (
<main>
<h1>Blog: {posts.length} posts</h1>
<Suspense fallback={<p>Loading your feed...</p>}>
<PersonalFeed />
</Suspense>
</main>
)
}The heading and the cached post count prerender into the shell, because getLatestPosts is cached and resolves at build time. The personal feed reads a cookie, so it streams in at request time. This is Partial Prerendering, and it is the default when Cache Components is on.
The pre-Cache-Components model
Older Next.js used route segment config such as export const dynamic = 'force-dynamic' and export const revalidate to control rendering per route. That model still works when Cache Components is disabled, but it is a different system from the component-level decision described here.
If you are keeping the older model, see caching without Cache Components. The modern path needs no per-route config, because one page already mixes both modes automatically.
Rune AI
Key Insights
- Static rendering finishes at build time and can be served from a CDN.
- Dynamic rendering happens at request time because the data is not known before the request.
- Cache Components decide per component, not per route.
- Uncached reads and request-time APIs stream behind Suspense boundaries.
- The old route segment config model is separate from Cache Components.
Frequently Asked Questions
Does a route have to be fully static or fully dynamic?
Does reading cookies() make the whole page dynamic?
Conclusion
Static rendering means work that finishes at build time, and dynamic rendering means work that waits for a request. Next.js 16 decides per component based on the APIs you use, so one route is usually a mix of both.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.