Blog

Rendering in Next.js

A sphynx cat in sunglasses sitting in front of the oversized red word nextjs, one paw resting on a closed laptop

Someone produced the HTML your browser receives. The question is: when did they produce it?

The answer is roughly one of four. At build time. When the user asked. It was produced earlier and refreshed in the background. Or it was never produced at all, and the browser drew it itself.

In modern Next.js this is less about "picking a strategy" and more about asking each piece of content "when should this be produced?". Below are the five approaches, in order.

CSR (Client-Side Rendering)

Writing "use client" in the App Router does not make a component CSR on its own. The component can still be rendered on the server on the first request and included in the HTML. If you genuinely want to turn server rendering off and move rendering to the client, you use a dynamic import with ssr: false.

CSR makes sense for interactive screens that use window / document, are browser-specific, or where SEO doesn't matter. But if SEO and a fast first render do matter, leaving everything to CSR is usually a bad trade: the browser first gets an empty shell, downloads and runs the JS, fetches the data in a separate request, and only then is the content drawn.

Client-side rendering in five steps: you request the page, an empty HTML shell arrives, the browser downloads and runs the JavaScript, the JavaScript requests the data, and only then is the content drawn

SSG (Static Site Generation)

The page is generated at build time. Data is fetched during the build, and the resulting HTML is produced statically and deployed to the CDN. When a user makes a request, no data is fetched on the server again; the CDN returns the ready HTML directly.

Because of this, TTFB (Time to First Byte) is not affected by how long a data fetch takes at request time.

The downside is that updating the content requires another build and deploy, and with very large datasets the build time can grow significantly.

Static site generation: at build time the data is fetched, the HTML is generated and copied to the CDN, all before any user arrives; when you request the page the CDN returns the ready file

ISR (Incremental Static Regeneration)

ISR sits between SSG and SSR by letting static pages be regenerated under certain conditions. The page is generated at build time, and revalidate sets its cache lifetime.

revalidate: 60 does not mean "refresh every 60 seconds". After the window expires, the first request to arrive gets the existing HTML and a new page is generated in the background. The next request gets the new content.

So ISR uses request-driven regeneration; no traffic means no regeneration.

revalidateTag can trigger cache invalidation without waiting for the window to expire.

The downside: for data that has to be accurate right now, like stock levels, live prices, or remaining seats, a user seeing stale data is not acceptable.

Incremental static regeneration: the HTML is built with an expiry, user A arrives before it and gets the page, user B arrives after expiry and still gets the old page while their request triggers a background rebuild, and user C gets the freshly regenerated page

SSR (Server-Side Rendering)

When a user requests the page, Next.js fetches the data it needs on the server, produces the HTML, and sends it back as the response.

In the App Router, SSR matters especially for data that is request-dependent or needs to be current. When you use dynamic APIs like cookies(), headers(), searchParams, or an uncached data fetch, the route is rendered dynamically.

In return, because server-side work happens on every request, TTFB is affected by the data source's latency. SSR gives you current and personalized content, but the cost is server-side rendering per request.

Server-side rendering: on every request you ask for the page, the server fetches the data, the server builds the HTML, and the content is drawn — the whole loop repeats for each request

PPR (Partial Prerendering)

PPR is a rendering approach that combines a page's static and dynamic sections in the same response.

The static shell is prepared during the build, with placeholders left for the dynamic sections. When a user makes a request, the shell is sent immediately; the dynamic parts are filled into the same response as the server prepares them.

In the earlier strategies a page was treated as static or dynamic as a whole. With PPR you can render only the part of the page that is genuinely dynamic at request time.

In Next.js 16, cacheComponents: true shifts the model further: you mark what should be cached explicitly with use cache.

Watch out: if dynamic content isn't separated by a Suspense boundary, it can block the whole shell. Fallback design also matters; the user sees the fallback first, then the real content.

Partial prerendering: a static shell with empty dynamic slots is built ahead of time; when you request the page the shell arrives instantly, the server prepares the dynamic parts, and the slots fill in within the same response

TL;DR

In practice the question is no longer "which rendering strategy should I use?". You look at the content itself first:

  • Is this content specific to the user?
  • How often does it change, and is it acceptable for someone to see an old version?
  • More importantly, does this dynamic part really need to hold up the rest of the page?

The answers decide whether the content is prepared at build time, cached and refreshed on some schedule, or rendered on every request. In the modern Next.js rendering approach the goal isn't to pick a single strategy, it's to render each piece of content at the right time for what it needs.