
Plain React runs in the browser. Frameworks like Next.js add routing, server rendering, data fetching and build optimisations on top of it, and "explain CSR vs SSR vs SSG vs ISR" is now one of the most common frontend interview questions. So are React Server Components, hydration errors and Next.js caching.
This is Part 13 of the Learn Frontend series, after State Management and Data Fetching. We compare the rendering strategies, then cover how the Next.js App Router implements them: Server and Client Components, streaming, caching and revalidation, Server Actions, middleware and built-in optimisations.
- The rendering strategies
- Hydration and hydration errors
- The App Router
- Server Components and Client Components
- Streaming with Suspense
- Data fetching, caching and revalidation
- Server Actions and mutations
- Middleware and built-in optimisations
- Choosing a strategy
- Practice problems
- Interview questions and FAQ
1. The rendering strategies
The core question is where and when is the HTML produced?
| Strategy | HTML is built | Good for | Trade-off |
|---|---|---|---|
| CSR (client-side rendering) | In the browser, after JS downloads | Dashboards and apps behind login | Blank page until JS runs, weaker SEO, slower first paint |
| SSR (server-side rendering) | On the server for every request | Personalised or always-fresh pages | Server work per request, slower TTFB |
| SSG (static site generation) | Once at build time | Blogs, docs, marketing pages | Data is only as fresh as the last build |
| ISR (incremental static regeneration) | At build, then regenerated in the background after a time or on demand | Product and content pages with many URLs | Some users briefly see slightly stale content |
| Streaming SSR | On the server, sent in chunks as parts become ready | Pages with some slow data | Needs Suspense boundaries planned well |
A useful way to say it in an interview: SSG and ISR give the fastest responses because HTML comes from a CDN cache; SSR gives the freshest data; CSR moves the work to the user's device. Real apps mix them per route, and modern Next.js even mixes them inside one page.
2. Hydration and hydration errors
With SSR or SSG, the browser first gets HTML that looks right but is not interactive. Then React downloads, runs the components again on the client and attaches event handlers to the existing DOM. This step is hydration.
- A hydration mismatch happens when the client render produces different HTML from the server: using
Date.now(),Math.random(),windoworlocalStorageduring render, locale-dependent formatting, or invalid HTML nesting like a<div>inside a<p>. - Fixes: move browser-only code into
useEffect, render it only after mount, usesuppressHydrationWarningfor unavoidable cases like timestamps, or load the component withdynamic(() => import(...), { ssr: false }). - Hydration costs JavaScript and CPU time, which hurts INP on slow phones. Server Components help by sending less JavaScript in the first place.
3. The App Router
Since Next.js 13 the App Router (the app/ directory) is the recommended way to build. Routing is file-based:
app/
layout.tsx -> root layout (html, body), wraps every page
page.tsx -> "/"
loading.tsx -> fallback while "/" loads
blog/
page.tsx -> "/blog"
[slug]/
page.tsx -> "/blog/my-first-post"
error.tsx -> error boundary for this segment
(marketing)/
about/page.tsx -> "/about" (group name not in URL)
api/
posts/route.ts -> "/api/posts" (GET, POST handlers)page.tsxmakes a route public.layout.tsxwraps child routes and keeps its state across navigations.loading.tsxis an automatic Suspense fallback;error.tsxis an Error Boundary (it must be a Client Component);not-found.tsxhandles 404s.- Folders in brackets are dynamic segments (
[slug]), folders in parentheses are route groups that do not affect the URL, androute.tsdefines an API endpoint. - The older
pages/directory withgetServerSidePropsandgetStaticPropsstill works. See the App Router vs Pages Router comparison.
4. Server Components and Client Components
In the App Router, every component is a React Server Component (RSC) by default. It runs only on the server and its code is never sent to the browser.
// app/products/page.tsx -> Server Component (default)
import { db } from "@/lib/db";
import AddToCart from "./AddToCart";
export default async function ProductsPage() {
const products = await db.product.findMany(); // runs on the server only
return (
<ul>
{products.map(p => (
<li key={p.id}>
{p.name} - {p.price}
<AddToCart id={p.id} /> {/* interactive leaf */}
</li>
))}
</ul>
);
}
// app/products/AddToCart.tsx -> Client Component
"use client";
import { useState } from "react";
export default function AddToCart({ id }: { id: string }) {
const [added, setAdded] = useState(false);
return <button onClick={() => setAdded(true)}>{added ? "Added" : "Add to cart"}</button>;
}| Server Components | Client Components ("use client") |
|---|---|
| Can be async and await data directly | Can use state, effects and refs |
| Can read databases, files and secrets safely | Can use event handlers like onClick |
| Add zero JavaScript to the bundle | Can use browser APIs such as window and localStorage |
| Cannot use hooks or browser APIs | Their code ships to the browser and hydrates |
"use client"marks a boundary. That file and everything it imports become client code. Put the boundary as low in the tree as you can, on small interactive leaves.- A Server Component can pass a Server Component to a Client Component as
children; the Client Component just places the already-rendered result. - Props passed from server to client must be serialisable: no functions (except Server Actions), class instances or Dates without conversion.
- Client Components still render on the server for the first HTML. "Client" means "also hydrates in the browser", not "renders only in the browser".
More detail: React Server Components explained.
5. Streaming with Suspense
Without streaming, an SSR page waits for its slowest query before sending anything. With streaming, Next.js sends the shell immediately and fills in each <Suspense> boundary as its data arrives.
import { Suspense } from "react";
export default async function ProductPage({ params }) {
const { id } = await params; // params is a promise in Next.js 15+
return (
<>
<ProductDetails id={id} /> {/* fast, sent first */}
<Suspense fallback={<p>Loading reviews...</p>}>
<Reviews id={id} /> {/* slow, streamed in later */}
</Suspense>
</>
);
}
async function Reviews({ id }) {
const reviews = await getReviews(id); // slow API
return <ul>{reviews.map(r => <li key={r.id}>{r.text}</li>)}</ul>;
}This improves TTFB and LCP, because the header and main content appear while a slow "recommendations" widget is still loading. Start independent requests in parallel (for example with Promise.all) to avoid server-side waterfalls. Partial Prerendering takes this further: a static shell is served from the CDN instantly, with dynamic holes streamed in.
6. Data fetching, caching and revalidation
Next.js decides per route whether it is static (rendered at build or cached) or dynamic (rendered per request). Using request-time data such as cookies(), headers() or searchParams makes a route dynamic.
// Static page regenerated at most once an hour (ISR)
export const revalidate = 3600;
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map(p => ({ slug: p.slug })); // pre-render each blog post
}
export default async function Post({ params }) {
const { slug } = await params;
const post = await fetch("https://cms.example.com/posts/" + slug, {
next: { tags: ["posts"] }, // tag for on-demand revalidation
}).then(r => r.json());
return <article><h1>{post.title}</h1></article>;
}
// Elsewhere, after the CMS publishes: revalidateTag("posts");- Time-based revalidation (ISR):
export const revalidate = 3600orfetch(url, { next: { revalidate: 3600 } }). - On-demand revalidation: call
revalidatePath("/blog")orrevalidateTag("posts")after a CMS webhook or a mutation. generateStaticParamspre-renders dynamic routes such as/blog/[slug]at build time.- Defaults changed over time. In Next.js 14,
fetchwas cached by default. Next.js 15 madefetchand GET route handlers uncached by default, so you opt in to caching. Next.js 16 introduced Cache Components with the"use cache"directive for explicit caching. In interviews, say which version you mean. - On the client, the Router Cache keeps visited segments for fast back and forward navigation, and
<Link>prefetches routes in the viewport.
7. Server Actions and mutations
A Server Action is an async function marked with "use server" that runs on the server but can be called from a form or a Client Component. Next.js creates the API endpoint for you.
// app/posts/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const schema = z.object({ title: z.string().min(3) });
export async function createPost(formData: FormData) {
const session = await auth();
if (!session) throw new Error("Unauthorized"); // always check auth
const parsed = schema.safeParse({ title: formData.get("title") });
if (!parsed.success) return { error: "Title is too short" };
await db.post.create({ data: { title: parsed.data.title, authorId: session.user.id } });
revalidatePath("/posts"); // refresh the list
}
// app/posts/new/page.tsx
import { createPost } from "../actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}- Forms with
action={createPost}work even before JavaScript loads (progressive enhancement). - Combine with
useActionStatefor errors anduseFormStatusfor pending buttons (Part 11). - Server Actions are public HTTP endpoints. Always validate input and check authentication and authorisation inside the action, exactly as you would in an API route.
8. Middleware and built-in optimisations
- Middleware (
middleware.ts, renamedproxy.tsin Next.js 16) runs before a request reaches a route. It is used for redirects, rewrites, locale detection, A/B tests and lightweight auth checks. Keep it fast; do not rely on it alone for authorisation. - next/image resizes, converts to WebP or AVIF, lazy-loads and reserves space to prevent layout shift. Set
priorityon the LCP image. - next/font self-hosts fonts and avoids layout shift from font swapping.
- Metadata API: export
metadataorgenerateMetadatafrom a page for titles, descriptions and Open Graph tags, which matters for SEO. - Automatic code splitting per route, plus
next/dynamicfor lazy-loading heavy client components.
Related: Core Web Vitals for React and Next.js.
9. Choosing a strategy
- Marketing page, blog, docs: static (SSG), revalidated on demand when content changes.
- E-commerce product pages: static with ISR or tag revalidation; price and stock widgets streamed or fetched dynamically.
- Search results with filters: dynamic SSR reading
searchParams, with streaming. - Logged-in dashboard: dynamic rendering with Server Components for data and small Client Components for charts and interactions, or plain CSR if SEO does not matter.
- Real-time chat or collaborative editor: mostly client-rendered with WebSockets, inside a server-rendered shell.
- CSR renders in the browser, SSR per request, SSG at build time, ISR at build and then in the background. Streaming sends HTML in chunks.
- Hydration attaches React to server HTML. Mismatches come from non-deterministic or browser-only code in render.
- In the App Router, components are Server Components by default. Add "use client" only on small interactive leaves.
- Server Components can fetch data directly and ship no JavaScript; Client Components handle state, effects and events.
- Suspense boundaries let Next.js stream slow parts of a page without blocking the rest.
- Use revalidate, revalidatePath and revalidateTag for ISR, and know that caching defaults changed between Next.js 14, 15 and 16.
- Server Actions are public endpoints: validate input and check auth inside every one.
Practice problems
Build a small Next.js app that uses every strategy at least once.
- Create a blog with SSG for posts using generateStaticParams, then add ISR with a 60 second revalidate.
- Add on-demand revalidation with a route handler that calls revalidateTag when a secret webhook is hit.
- Build a product page where details render immediately and reviews stream in behind a Suspense boundary.
- Create a hydration mismatch on purpose with Date.now(), read the error, then fix it.
- Build a "create post" form with a Server Action, Zod validation, useActionState errors and revalidatePath.
- Use next/image with priority for the hero image and measure LCP before and after.
Interview questions and FAQ
What is the difference between SSR and SSG?
SSR builds the HTML on the server for every request, so data is always fresh but each request costs server time. SSG builds the HTML once at build time and serves it from a CDN, which is faster and cheaper but only as fresh as the last build or revalidation.
What is ISR?
Incremental Static Regeneration serves a static page and regenerates it in the background after a set time or when you trigger revalidation. Users get static speed, and content updates without a full rebuild.
What are React Server Components?
Components that run only on the server. They can be async, read data directly and send rendered output to the client without shipping their own JavaScript. Interactive parts are marked "use client" and hydrate in the browser.
What causes a hydration error?
The HTML rendered on the client differs from the server HTML, usually because of random values, dates, browser-only APIs like window during render, or invalid HTML nesting. Move such code into effects or render it only on the client.
Does "use client" mean the component only renders in the browser?
No. Client Components are still rendered to HTML on the server for the first load. The directive means the component's code is also sent to the browser so it can hydrate and handle state and events.
How do you make a Next.js page dynamic?
Use request-time APIs such as cookies(), headers() or searchParams, set export const dynamic = "force-dynamic", or fetch with cache: "no-store". Otherwise Next.js tries to render the route statically.
Comments
Post a Comment