Introduction 👋
Rendering is the process of turning your React code into HTML that browsers can display. Next.js offers a spectrum of rendering strategies — from fully static to fully dynamic — and this flexibility is one of its biggest strengths. This tutorial covers how rendering works under the hood, and how to choose the right strategy for each part of your app.
Information
What is Rendering? 🧠
Rendering converts your React components into HTML markup. In Next.js, this can happen at build time, on the server per-request, or in the browser — and often a mix of all three within a single page.
- Where rendering happens: server or client.
- When rendering happens: build time, request time, or after a cache expires.
Rendering Pipeline 🏗️
A typical Next.js request flows through several stages: matching a route, rendering Server Components into a special data format (RSC Payload), converting that into HTML, sending HTML to the browser, and finally hydrating it into an interactive app.
Client-Side Rendering (CSR) 💻
In Client-Side Rendering, the browser downloads a minimal HTML shell and JavaScript bundle, then renders the UI entirely in the browser. This is the traditional React SPA model.
components/Counter.tsx
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Caution
Server-Side Rendering (SSR) 🖥️
Server-Side Rendering generates the full HTML for a page on the server, on every request. This ensures content is always fresh and immediately visible, at the cost of a server round-trip per visit.
app/dashboard/page.tsx
export const dynamic = "force-dynamic";
export default async function Dashboard() {
const data = await fetchLiveData();
return <p>{data.value}</p>;
}Static Site Generation (SSG) 📄
With Static Site Generation, pages are rendered to HTML once at build time and reused for every request, making them extremely fast to serve since no per-request computation is needed.
app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <h1>{post.title}</h1>;
}Tip
Incremental Static Regeneration (ISR) ♻️
ISR lets you update statically generated pages after build time, on a set interval, without rebuilding the entire site. It combines the speed of static generation with the freshness of dynamic data.
app/products/[id]/page.tsx
export const revalidate = 3600; // regenerate every hour
export default async function Product({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return <h1>{product.name}</h1>;
}Partial Prerendering (PPR) 🧩
Partial Prerendering is an experimental strategy that combines a static shell, rendered at build time, with dynamic holes that stream in at request time — giving both instant static content and fresh dynamic data in a single page.
app/product/[id]/page.tsx
import { Suspense } from "react";
export const experimental_ppr = true;
export default function Product({ params }: { params: { id: string } }) {
return (
<div>
<StaticProductInfo />
<Suspense fallback={<p>Loading reviews…</p>}>
<DynamicReviews id={params.id} />
</Suspense>
</div>
);
}Note
Dynamic Rendering 🔄
Dynamic Rendering renders a route on the server for every incoming request, using the latest data. Next.js automatically switches a route to dynamic rendering when it detects request-specific APIs.
- Using cookies() or headers().
- Reading the searchParams prop.
- Setting export const dynamic = "force-dynamic".
- Using an uncached fetch() request.
Hybrid Rendering 🧬
Next.js apps are typically hybrid by nature — some routes are static, others dynamic, and some combine both through PPR or nested <Suspense> boundaries, all within the same application.
| Route | Strategy |
|---|---|
| / | Static (SSG) |
| /blog/[slug] | Static + ISR |
| /dashboard | Dynamic (SSR) |
Streaming 🌊
Streaming allows the server to send HTML to the browser incrementally, as it becomes ready, rather than waiting for the entire page to finish rendering. This is built on <Suspense> boundaries.
app/dashboard/page.tsx
import { Suspense } from "react";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading stats…</p>}>
<Stats />
</Suspense>
</div>
);
}Best Practice
Hydration 💧
Hydration is the process where React attaches event listeners and internal state to the server-rendered HTML in the browser, turning static markup into a fully interactive application.
Warning
React Server Components 🧠
React Server Components (RSC) render exclusively on the server and never ship their JavaScript to the browser, resulting in smaller client bundles. They can directly access backend resources like databases without an API layer.
app/posts/page.tsx
export default async function Posts() {
const posts = await db.query("SELECT * FROM posts");
return <List type="unordered">{posts.map((p) => <List.Item key={p.id}>{p.title}</List.Item>)}</List>;
}Client Components 🖱️
Client Components, marked with the "use client" directive, render on the server for the initial HTML and then hydrate in the browser to support interactivity like state, effects, and event handlers.
components/LikeButton.tsx
"use client";
import { useState } from "react";
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? "❤️" : "🤍"}</button>;
}Server Components 🗄️
Server Components are the default in the App Router — every component is a Server Component unless explicitly marked with "use client". They're ideal for data fetching and content that doesn't require interactivity.
| Capability | Server | Client |
|---|---|---|
| Direct database access | ✅ | ❌ |
| useState / useEffect | ❌ | ✅ |
| Event handlers (onClick) | ❌ | ✅ |
| Zero client JS shipped | ✅ | ❌ |
Rendering Strategies 🗺️
Choosing a rendering strategy is about matching how often data changes with how it's rendered. Next.js supports mixing strategies at the individual route segment level.
- Static — content rarely changes (marketing pages, docs).
- ISR — content changes occasionally (product catalogs, blogs).
- Dynamic — content changes per-request or per-user (dashboards, feeds).
Static Rendering 🧊
Static Rendering is the default in the App Router. As long as a route doesn't use dynamic APIs, Next.js renders it once at build time and serves the cached result to every visitor.
app/about/page.tsx
export default function About() {
return <h1>About Us</h1>;
}Dynamic Rendering Triggers ⚡
Certain APIs and configurations automatically opt a route out of static rendering. Recognizing these triggers helps you understand why a route you expected to be static is actually dynamic.
| Trigger | Effect |
|---|---|
| cookies() | forces dynamic rendering |
| headers() | forces dynamic rendering |
| searchParams prop | forces dynamic rendering |
| uncached fetch() | forces dynamic rendering |
Route Segment Configuration ⚙️
Exported configuration constants let you explicitly control a route segment's rendering behavior, overriding Next.js's automatic detection.
app/page.tsx
export const dynamic = "force-static"; // or "force-dynamic", "auto", "error"
export const revalidate = 3600;Rendering Performance 🚀
- Prefer static rendering wherever content doesn't need to change per-request.
- Use <Suspense> to stream slow data instead of blocking the whole page.
- Push interactivity down into small Client Components rather than marking entire trees as client-side.
- Cache fetch() calls where data doesn't need to be request-fresh.
SEO Considerations 🔍
Because Next.js renders HTML on the server (via SSR, SSG, or ISR), content is present in the initial response, making it fully crawlable by search engines — unlike pure client-side rendered apps.
Tip
Caching and Rendering 🗃️
Rendering and caching are closely linked: statically rendered pages are cached at the data cache and full route cache layers, while dynamic pages typically bypass these caches unless explicitly configured.
| Cache Layer | What it Stores |
|---|---|
| Request Memoization | deduplicated fetch() calls within a single render |
| Data Cache | persisted fetch() results across requests |
| Full Route Cache | rendered HTML and RSC payload for static routes |
| Router Cache | client-side cache of visited route segments |
Revalidation 🔁
Revalidation refreshes cached data or pages without a full rebuild. Next.js supports both time-based revalidation and on-demand revalidation triggered by code.
app/actions.ts
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function updatePost() {
await savePost();
revalidatePath("/blog");
revalidateTag("posts");
}Rendering Debugging 🐞
Next.js provides build-time output indicating whether each route is ○ (static), ƒ (dynamic), or incremental — a quick way to verify a route is rendering the way you expect.
Hint
Choosing the Right Rendering Strategy 🧭
Content rarely changes and is identical for every visitor — a perfect fit for Static Rendering with no revalidate needed.
Content updates occasionally when new posts are published — ISR with a sensible revalidate window keeps pages fast and reasonably fresh.
Content is user-specific and must always be current — Dynamic Rendering ensures every request reflects the latest state.
Best Practices ✅
- Default to Server Components; add "use client" only where interactivity is required.
- Use <Suspense> boundaries to enable streaming for slow data-fetching sections.
- Prefer ISR over full force-dynamic when data doesn't need to be real-time.
- Use revalidateTag for precise, on-demand cache invalidation after mutations.
Common Mistakes ⚠️
- Marking entire pages as "use client" when only a small part needs interactivity.
- Accidentally opting a route into dynamic rendering by calling cookies() unnecessarily.
- Forgetting <Suspense> boundaries, causing the whole page to block on slow data.
- Assuming ISR updates instantly — it respects the configured revalidate window.