Suggest an editImprove this articleRefine the answer for “How server-side rendering (SSR) works in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Server-side rendering (SSR)** in Next.js generates full HTML on the server for every request, using live data, before sending it to the browser for React hydration. ```tsx const res = await fetch('/api/data', { cache: 'no-store' }); ``` **Key point:** the browser receives real content on the first response, without waiting for client JavaScript to fetch data.Shown above the full answer for quick recall.Answer (EN)Image**Server-side rendering (SSR)** in Next.js generates a complete HTML page on the server for every incoming request, pulling live data before sending the result to the browser. ## Theory ### TL;DR - SSR is like a restaurant cooking your meal to order. SSG pre-makes batches and hands them out. - Every request triggers fresh data fetching and a full HTML render on the server. - In the App Router, add `cache: 'no-store'` to a fetch call or call `cookies()` / `headers()` to opt into SSR automatically. - Use SSR for personalized or frequently changing data. For anything cacheable, SSG or ISR costs less. ### Quick example ```tsx // app/dashboard/page.tsx async function Dashboard() { const res = await fetch('https://api.example.com/user-data', { cache: 'no-store' // fresh data per request = SSR behavior }); const data = await res.json(); return <div>Balance: {data.balance}</div>; } ``` On every request Next.js runs this function on the server, fetches live data, renders `<div>Balance: $42.50</div>`, and streams the full HTML to the browser. JavaScript arrives later and React hydrates the page for interactivity. ### Key difference SSR differs from SSG in one thing: timing. SSG builds HTML once at deploy time and serves it instantly from a CDN. SSR builds HTML on demand, per request, on the server. That makes the first byte slower but the data always accurate. Compared to client-side rendering (CSR), SSR delivers real content in the initial HTML, so search crawlers see it immediately and users get a meaningful first paint without waiting for JavaScript to run. ### When to use - **Personalized per user** - profile pages, dashboards with session data, anything behind a login. - **Real-time external data** - live prices, scores, inventory counts that change by the minute. - **Auth-dependent content** - pages that read cookies or headers to decide what to show. - **Static or rarely changed content** - pick SSG, it is faster and puts zero load on the server. - **High traffic, data can be a minute old** - ISR with `revalidate` is a better fit than SSR. ### Comparison table | Feature | SSR | SSG | CSR | |---|---|---|---| | Data fetched | Per request (server) | At build time | After page load (client) | | Initial HTML | Fully rendered | Pre-built static file | Empty shell | | TTFB | Higher (server compute) | Fastest (CDN) | Fast but no real content | | SEO | Excellent | Excellent | Poor without pre-rendering | | Server load | Per request | None after build | Low | | Best for | User-specific / live data | Marketing, docs | App-like dashboards post-login | ### How it works internally A request hits the Node.js server. Next.js identifies the page as dynamic, either by `cache: 'no-store'`, a call to `cookies()` or `headers()`, or `export const dynamic = 'force-dynamic'`. The async server component runs, fetches data via HTTP or directly from a database. React converts the component tree to an HTML string with `ReactDOMServer.renderToString()`. That HTML ships to the browser. The browser downloads JS bundles, then `ReactDOM.hydrateRoot()` attaches event handlers to the existing DOM without re-rendering everything. Streaming changes this slightly. With React 18 and the App Router, Next.js sends the HTML shell first and streams chunks as each `Suspense` boundary resolves, so a slow API call does not block the whole page. ### Common mistakes **Calling `cookies()` inside a client component** ```tsx // Wrong - throws "Headers cannot be used in Client Components" 'use client'; import { cookies } from 'next/headers'; const session = cookies().get('session'); // error // Fix - read cookies in a server component, pass down as props async function ServerLayout() { const session = cookies().get('session'); return <ClientNav session={session?.value} />; } ``` `next/headers` is server-only. Client components run in the browser where request headers do not exist. **Forgetting `cache: 'no-store'` and getting stale data** ```tsx // Wrong - Next.js caches this by default, data gets stale const data = await fetch('/api/live-prices'); // Fix const data = await fetch('/api/live-prices', { cache: 'no-store' }); ``` The App Router caches `fetch` by default. This surprises most teams moving from the Pages Router, where fetch was uncached. Without `cache: 'no-store'`, your page may return data from the build rather than the current request. **Blocking the whole page on a slow API** ```tsx // Wrong - entire page waits for Stripe before any HTML ships export default async function OrdersPage() { const res = await fetch('https://api.stripe.com/v1/orders', { cache: 'no-store' }); // user waits 2+ seconds for the first byte } // Fix - stream the slow part, ship the shell immediately import { Suspense } from 'react'; export default function OrdersPage() { return ( <div> <h1>Your Orders</h1> <Suspense fallback={<p>Loading orders...</p>}> <SlowOrders /> </Suspense> </div> ); } ``` Without a `Suspense` boundary, a 2-second Stripe response means a 2-second TTFB and a bad Lighthouse score. **Using SSR on every route by default** Every SSR request runs server code. On high traffic that is expensive and scales poorly. Pages with the same content for every user, blog posts, landing pages, documentation, should be SSG. Keep SSR for pages where the data genuinely differs per user or per request. ### Real-world usage - **Vercel dashboard** - billing data pulled from the Stripe API per user session. - **Netflix** - personalized watchlists rendered server-side using per-request user preferences. - **GitHub** - repository pages with live commit counts fetched per request. - **IT Lead profile** - session cookie read via `cookies()`, DB query for solved problems, rendered fresh on every visit. ### Follow-up questions **Q:** How does SSR differ from ISR? **A:** ISR builds HTML statically but regenerates it in the background on a schedule, for example `revalidate: 60` means rebuild at most once per minute. SSR rebuilds HTML from scratch on every single request. ISR is cheaper; SSR is always current. **Q:** What happens during a hydration mismatch? **A:** React logs a warning in the browser console and may re-render the affected subtree on the client. The usual cause is server and client fetching different data. Fix it by making sure both sides use the same source. **Q:** What is the difference between React Server Components and SSR? **A:** SSR is a rendering strategy: generate HTML on the server per request. React Server Components (RSC) are a component model: they run only on the server, fetch data inline, and send zero JavaScript to the client. In the Next.js App Router they work together, but they are not the same concept. **Q:** What happens at the Node.js level when an SSR request arrives? **A:** Next.js runs the async server component tree. Node fetches data via `undici` for HTTP or a DB driver directly. React calls `renderToString()` or `renderToPipeableStream()` for streaming. The HTML flushes through the HTTP response. The browser downloads the JS bundle and `hydrateRoot()` attaches event listeners. **Q:** A slow DB query is adding 1.5 seconds to TTFB. Walk through how you would fix it. **A:** First, wrap the slow component in a `Suspense` boundary so the HTML shell ships immediately. Then check if the query field has an index and whether you are selecting more data than needed. If the result is not user-specific, move the component to ISR with a short revalidation window. Profile with Vercel Speed Insights or a `console.time()` wrapper to confirm where the time actually goes. ## Examples ### Basic: opt into SSR with `cache: 'no-store'` ```tsx // app/prices/page.tsx export default async function PricesPage() { const res = await fetch('https://api.example.com/prices', { cache: 'no-store' // Next.js will not cache this response }); const prices = await res.json(); return ( <ul> {prices.map((item: { id: string; name: string; price: number }) => ( <li key={item.id}>{item.name}: ${item.price}</li> ))} </ul> ); } ``` No extra configuration needed. The `cache: 'no-store'` flag is enough for Next.js to skip the static build for this route and fetch live data on every request. ### Intermediate: auth check with session cookie (App Router) ```tsx // app/profile/page.tsx import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; import { db } from '@/lib/db'; export default async function ProfilePage() { const cookieStore = cookies(); // marks the route as dynamic automatically const sessionId = cookieStore.get('session')?.value; if (!sessionId) redirect('/login'); const user = await db.user.findUnique({ where: { sessionId } }); return ( <div> <h1>Welcome, {user?.name}</h1> <p>Problems solved: {user?.solvedCount}</p> </div> ); } ``` Calling `cookies()` from `next/headers` signals to Next.js that this page depends on the incoming request. No `cache: 'no-store'` required. The page queries the database fresh on every visit, auth check included. ### Advanced: streaming SSR with Suspense for a slow external API ```tsx // app/orders/page.tsx import { Suspense } from 'react'; async function OrderList() { // This fetch blocks only OrderList, not the whole page const res = await fetch('https://api.stripe.com/v1/charges', { cache: 'no-store' }); const { data: charges } = await res.json(); return ( <ul> {charges.map((c: { id: string; amount: number }) => ( <li key={c.id}>${(c.amount / 100).toFixed(2)}</li> ))} </ul> ); } export default function OrdersPage() { return ( <div> <h1>Your Orders</h1> {/* Shell ships immediately. OrderList streams in when ready. */} <Suspense fallback={<p>Loading orders...</p>}> <OrderList /> </Suspense> </div> ); } ``` The `<h1>` and the fallback text reach the browser before the Stripe response arrives. Once `OrderList` resolves, React streams the finished HTML and swaps the fallback out. TTFB stays low even when the upstream API is slow.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.