Next.js rendering patterns: CSR, SSR, RSC, and Partial Prerendering explained
When you start a new Next.js project with the App Router, you immediately face a set of decisions about how your pages and components should render. Choose wrong and you are trading SEO for interactivity, or performance for developer convenience. This guide walks you through the 4 rendering patterns available to you in modern Next.js, what each one does, and when to reach for it.
The strategies are: Client Side Rendering (CSR), Server Side Rendering (SSR), React Server Components (RSC), and Partial Prerendering (PPR). Each targets a different point on the speed, interactivity, and freshness tradeoff.
The rendering pattern menu in modern Next.js
Before diving in, here is how these patterns map to the fundamental tension in web apps.
Static HTML is fast because the browser gets a complete page and can start painting immediately. But if the data is stale, the user sees outdated content. A dynamic server response is fresh but adds latency on every request. A client rendered page is interactive but starts as an empty shell, which hurts TTFB and SEO because crawlers see nothing until JavaScript runs.
Next.js does not force you to pick one approach for the entire app. You can mix patterns per route, or per component, within the App Router.
CSR: shipping an empty shell and hydrating
In CSR, the server sends a minimal HTML document with a JavaScript bundle. The browser downloads and runs that bundle, which then fetches data and renders the UI. This is how the original Create React App workflow operated.
In the Next.js App Router, you opt a component into client side rendering with the "use client" directive at the top of the file:
"use client"
import { useState } from "react"
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
}
The benefit is full interactivity and access to browser APIs. The tradeoff is that the initial HTML is empty, which delays First Contentful Paint and can hurt your search ranking if your crawled content lives inside this component.
CSR is well suited for dashboards behind authentication, highly interactive widgets, or anything that genuinely needs browser APIs like window or navigator. For public content you want indexed, choose something else.
SSR: full HTML on every request
SSR generates a complete HTML page on the server for every incoming request. The user gets real, readable HTML immediately, which improves TTFB and makes content crawlable. After the HTML loads, React hydrates it so the interactive parts wake up.
In the App Router, you get SSR by making a component async and using cache: "no-store" on the fetch:
// app/products/page.tsx
export default async function ProductsPage() {
const products = await fetch("https://api.example.com/products", {
cache: "no-store",
}).then(r => r.json())
return (
<ul>
{products.map((p: { id: string; name: string }) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}
Without cache: "no-store", Next.js caches the fetch result and you get a static response rather than a fresh one on each visit. SSR is well suited for pages where data changes frequently and every visitor needs the latest version: stock prices, live event pages, personalized feeds.
React Server Components in the App Router
React Server Components are a more fundamental shift than CSR or SSR. In the App Router, all components are Server Components by default. Client Components are opt in via the "use client" directive. Server Components render on the server without sending their JavaScript to the client.
This means you can do things in a Server Component that you would never do in a client bundle: query a database directly, read from the file system, use secrets that must never reach the browser.
// app/blog/[slug]/page.tsx
import { db } from "@/lib/db"
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await db.post.findUnique({ where: { slug: params.slug } })
if (!post) return <p>Post not found</p>
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
No "use client" here. The database query runs on the server, and the rendered HTML is what gets sent to the browser. Your db module and credentials never touch the client bundle.
The recommended pattern is to keep data fetching and business logic in Server Components, then push the interactive leaves down to Client Components only where you need state or event handlers.
Streaming with Suspense boundaries
Streaming solves a specific problem with SSR: one slow data source holding up the entire page. Without streaming, the server waits for every await to resolve before sending any HTML. The user stares at a blank screen.
With streaming, Next.js sends the static parts of the page immediately and streams in the dynamic parts as they resolve. You control the boundaries with React's Suspense:
import { Suspense } from "react"
import { ProductList } from "./ProductList"
import { Reviews } from "./Reviews"
export default function ProductPage() {
return (
<div>
<h1>Our Products</h1>
<Suspense fallback={<p>Loading products...</p>}>
<ProductList />
</Suspense>
<Suspense fallback={<p>Loading reviews...</p>}>
<Reviews />
</Suspense>
</div>
)
}
ProductList and Reviews can resolve independently. If products are ready in 200ms and reviews take 900ms, the user sees the product list almost immediately. The reviews section streams in when ready, without a full page reload.
This dramatically improves perceived performance for pages with multiple data sources at different speeds.
Partial Prerendering: static shell, dynamic holes
Partial Prerendering (PPR) is the newest addition to the Next.js rendering toolkit. It serves a static prerendered shell while streaming dynamic parts wrapped in Suspense within the same response.
Think of it as a middle ground between a fully static page and a fully dynamic one. Everything outside your Suspense boundaries is prerendered at build time and served from the edge instantly. The dynamic holes are filled in as the server resolves them, streamed into the same HTML response.
// app/product/[id]/page.tsx
import { Suspense } from "react"
import { StaticProductInfo } from "./StaticProductInfo"
import { DynamicInventory } from "./DynamicInventory"
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<main>
{/* Prerendered at build time — edge cached, serves instantly */}
<StaticProductInfo id={params.id} />
{/* Dynamic — streamed in on each request */}
<Suspense fallback={<span>Checking inventory...</span>}>
<DynamicInventory id={params.id} />
</Suspense>
</main>
)
}
PPR pairs well with product pages, landing pages, and any route where most content is static but a few components (inventory counts, personalized sections) need fresh data. The result is the speed of a static page for the shell, and the freshness of SSR only where it is actually needed.
Choosing a pattern per route
Here is a practical decision table:
| Route type | Recommended pattern |
|---|---|
| Marketing pages, blog posts | Static (default caching, no cache: "no-store") |
| Product pages with live inventory | PPR: static shell + Suspense for stock data |
| Authenticated dashboards with frequent updates | SSR with cache: "no-store" |
| Highly interactive forms, charts | Client Component with "use client" |
| Pages with multiple slow data sources | SSR + Suspense streaming |
The App Router makes it straightforward to mix patterns at the component level. A single page can be a Server Component at the top, have 2 Suspense boundaries around slow fetches, and nest a Client Component for an interactive chart.
A concrete example: a product listing page. The category header and filters are static and render from cache. The product grid is a Server Component reading from the database. The "Add to cart" button inside each card is a Client Component because it needs onClick. No single rendering strategy is forced on the entire route.
FAQ
What is the difference between CSR and SSR in Next.js?
In CSR, the server sends an empty HTML shell and the browser renders the page by running JavaScript. In SSR, the server generates complete HTML for every request and sends it to the browser, which then hydrates it. SSR improves TTFB and makes content crawlable by default. CSR suits highly interactive pages behind authentication where SEO is not a concern.
What are React Server Components?
React Server Components are components that render on the server and do not send their JavaScript to the client. In the Next.js App Router, all components are Server Components by default, and Client Components are opt in via the "use client" directive. Server Components can query databases, read from the file system, and access secrets directly because none of that code runs in the browser.
What is Partial Prerendering?
Partial Prerendering (PPR) is a Next.js rendering strategy that serves a static prerendered shell while streaming dynamic parts wrapped in Suspense within the same response. The shell is generated at build time and served instantly from the edge. The dynamic holes stream in from the server as data resolves. PPR gives you the speed of static rendering and the freshness of SSR, at the granularity of individual Suspense boundaries.

