Learn Frontend Part 13: Next.js and Rendering Strategies - CSR vs SSR vs SSG vs ISR, Server Components and Caching

Learn Frontend Part 13: Next.js and Rendering Strategies - CodingHumans

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.

1. The rendering strategies

The core question is where and when is the HTML produced?

StrategyHTML is builtGood forTrade-off
CSR (client-side rendering)In the browser, after JS downloadsDashboards and apps behind loginBlank page until JS runs, weaker SEO, slower first paint
SSR (server-side rendering)On the server for every requestPersonalised or always-fresh pagesServer work per request, slower TTFB
SSG (static site generation)Once at build timeBlogs, docs, marketing pagesData is only as fresh as the last build
ISR (incremental static regeneration)At build, then regenerated in the background after a time or on demandProduct and content pages with many URLsSome users briefly see slightly stale content
Streaming SSROn the server, sent in chunks as parts become readyPages with some slow dataNeeds 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(), window or localStorage during 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, use suppressHydrationWarning for unavoidable cases like timestamps, or load the component with dynamic(() => 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.tsx makes a route public. layout.tsx wraps child routes and keeps its state across navigations.
  • loading.tsx is an automatic Suspense fallback; error.tsx is an Error Boundary (it must be a Client Component); not-found.tsx handles 404s.
  • Folders in brackets are dynamic segments ([slug]), folders in parentheses are route groups that do not affect the URL, and route.ts defines an API endpoint.
  • The older pages/ directory with getServerSideProps and getStaticProps still 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 ComponentsClient Components ("use client")
Can be async and await data directlyCan use state, effects and refs
Can read databases, files and secrets safelyCan use event handlers like onClick
Add zero JavaScript to the bundleCan use browser APIs such as window and localStorage
Cannot use hooks or browser APIsTheir 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 = 3600 or fetch(url, { next: { revalidate: 3600 } }).
  • On-demand revalidation: call revalidatePath("/blog") or revalidateTag("posts") after a CMS webhook or a mutation.
  • generateStaticParams pre-renders dynamic routes such as /blog/[slug] at build time.
  • Defaults changed over time. In Next.js 14, fetch was cached by default. Next.js 15 made fetch and 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 useActionState for errors and useFormStatus for 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, renamed proxy.ts in 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 priority on the LCP image.
  • next/font self-hosts fonts and avoids layout shift from font swapping.
  • Metadata API: export metadata or generateMetadata from a page for titles, descriptions and Open Graph tags, which matters for SEO.
  • Automatic code splitting per route, plus next/dynamic for 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.
Key takeaways
  • 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.

  1. Create a blog with SSG for posts using generateStaticParams, then add ISR with a 60 second revalidate.
  2. Add on-demand revalidation with a route handler that calls revalidateTag when a secret webhook is hit.
  3. Build a product page where details render immediately and reviews stream in behind a Suspense boundary.
  4. Create a hydration mismatch on purpose with Date.now(), read the error, then fix it.
  5. Build a "create post" form with a Server Action, Zod validation, useActionState errors and revalidatePath.
  6. 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

Answered 0/0Correct 0