Next.js Rendering
Expert guidance on Next.js rendering modes, streaming behavior, and static vs dynamic page generation.
Static vs Dynamic Rendering
Next.js has two fundamental rendering types: static and dynamic.
Static rendering:
- Pages are built at build time and uploaded to a CDN
- No compute occurs at request time
- Assets are replicated to edge locations globally
- Faster response times (no compute, cached near user)
- Remains available even if origin region goes down
Dynamic rendering:
- Pages are executed on-demand in a configured region
- Involves compute at request time
- Goes down if the region becomes unavailable
- Required when content cannot be determined at build time
SSG (Static Site Generation) and ISR (Incremental Static Regeneration) are the same underlying mechanism — the only difference is whether all paths are generated upfront or incrementally on-demand.
Determining Render Mode: Async Dynamic APIs
Next.js determines whether a page is static or dynamic using async dynamic APIs. The framework performs a prospective render to detect what resolves synchronously vs asynchronously.
The microtask test:
- Next.js runs an initial "warmup render" to search for cached instances and fill caches
- A second render runs for just one tick, then aborts
- Anything that resolves within that microtask is considered "instant" and can be static
- Anything that cannot resolve in one tick is treated as dynamic
Special case for params:
Routes using params can be static because each route is called once per item in generateStaticParams. Since the params are known at build time, they resolve instantly.
// Static: params from generateStaticParams resolve instantly
export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }];
}
export default async function Page({ params }: { params: { id: string } }) {
// params.id is instant - this page can be static
}
Streaming and Suspense Boundaries
Streaming is only enabled for dynamic rendering. Static content does not stream because no compute is involved — the complete HTML is served from the CDN.
Document-level Suspense requirement: To enable streaming with a loading skeleton, wrap the HTML document with an empty Suspense boundary. Without a document wrapper outside the boundary, there is nothing to stream — the framework needs the outer shell to send while inner content loads.
// Enable streaming with document shell
<Suspense fallback={null}>
<html>
<body>
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
</body>
</html>
</Suspense>
ISR and Suspense fallbacks: Suspense boundary fallbacks do not display when generating ISR paths. Since ISR generation happens at build time or on first request (static generation), there is no streaming — the complete page is generated before being served.
Cache Components (use cache)
Cache components can combine static and dynamic rendering within the same page, but the page itself still has a render mode.
Applying use cache to a page component:
When use cache is added to a page or the main component of a page, the page produces static output that:
- Will be cached (static)
- Won't be streamed
- Won't have "holes" (Suspense boundaries) in it
Cache components with dynamic pages:
Even with use cache on individual components, the page overall is still either static or dynamic. Cache components allow granular control — static cached sections within dynamic pages, or dynamic sections within static pages.
// Page-level cache: produces static output
'use cache';
export default async function Page() {
// Entire page is static, no streaming
}
// Component-level cache: can mix static/dynamic
export default async function Page() {
return (
<>
<StaticCachedSection /> {/* has 'use cache' */}
<DynamicSection /> {/* no cache directive */}
</>
);
}
Rendering Workflow
When working with Next.js rendering:
- Determine if the page can be static (all data available at build time)
- If static, decide between full SSG (all paths) or ISR (incremental)
- If dynamic is required, plan Suspense boundaries for streaming
- Add document-level Suspense wrapper if streaming with skeletons
- Use
use cachegranularly to cache expensive components within dynamic pages - Remember: streaming only works for dynamic rendering, not static/ISR