NextJS App Router Rendering Strategies: CSR, SSR, SSG, ISR & PPR

Yusif Aliyev
8 min read English

This post is synced from my website using webhooks.

1. Full Page Client Side Rendering (CSR)

This one is very similar to how we used to build classic React apps before frameworks like NextJS.

When you use full Client Side Rendering, Next.js sends an almost empty HTML page. Then the browser runs JavaScript, fetches the data, and builds the page on the client. So the browser does all the work. The server just gives it the basic tools.

You might choose CSR when:

  • You don’t need SEO (for example, in dashboards or admin panels),
  • The page is very interactive,
  • The content is personalized for the logged-in user.
@/app/csr-example/page.tsx
"use client";

import { Post } from "@/lib/types";
import { useEffect, useState } from "react";

export default function Page() {
  const [post, setPost] = useState<Post | null>(null);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts/1")
      .then((res) => res.json())
      .then(setPost);
  }, []);

  if (!post) return <div>Loading…</div>;

  return (
      <div className="flex w-150 flex-col gap-y-5">
        <h2 className="text-2xl font-bold">{post.title}</h2>
        <p>{post.body}</p>
      </div>
  );
}

One downside of CSR is that the user might see a loading spinner for a while, especially on slower networks. Also, SEO bots usually can’t read JavaScript, so these pages don’t work well for search engines.

Maybe your page is very heavy has a lots of data manipulation, re-renders, and first-build. It can be good to do it in a server.

But for dynamic apps with lots of interaction, CSR is a good choice. And of course, you can always mix CSR with other strategies.

2. Server-Side Rendering (SSR)

With SSR, Next.js runs your code on the server and sends back a fully rendered HTML page. It fetches data on every request and includes everything in the HTML, including dynamic metadata.

This is useful for SEO and when your data changes often.

@/app/ssr-example/page.tsx
import { Post } from "@/lib/types";

export default async function Page() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", { cache: "no-store" });
  const items = ((await res.json()) as Post[]).slice(0, 10);
  return (
      <ul className="list-decimal">
        {items.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
  );
}

Here, the { cache: 'no-store' } tells Next.js to fetch fresh data every time. This keeps the content up to date.

Just a quick tip: when using Server Components like this, you can't use hooks like useState, useEffect, or anything that depends on the browser (like accessing window or navigator). If you need those, you’ll need to use a Client Component. I’ll write more about workarounds for that in a future post, so feel free to subscribe if you’re interested.

3. SSR + CSR Components

Many pages need both good SEO and some interactive features. For example, maybe you want to show a blog post (static content) but also allow users to like it (interactive).

In Next.js, you can mix Server Components (for data fetching and layout) with Client Components (for interactivity). This is a really powerful combo.

@/app/ssr-csr-example/page.tsx
import { LikeButton } from "@/components/LikeButton";
import { Post } from "@/lib/types";

export default async function Page() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts/1", { cache: "force-cache" });
  let post = (await res.json()) satisfies Post;
  post = { ...post, likes: 0 };
  
  return (
      <article className="flex flex-col items-center">
        <h1>{post.title}</h1>
        <LikeButton initialLikes={post.likes} />
      </article>
  );
}
@/app/components/LikeButton.tsx
// LikeButton.tsx (Client Component)
'use client' // Don't forget to add
import { useState } from 'react'

export default function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <button onClick={() => setLikes(likes + 1)}>
      ❤️ {likes}
    </button>
  )
}

The server renders the page with fresh data, and the client handles the like button’s interactivity.

4. Static Site Generation (SSG)

If your page doesn’t change often, and doesn’t use dynamic data — go with SSG.

Think of simple pages like Home Pages , About pages, Pricing tables, Static blog content.

Next.js will generate these pages at build time and serve them as pure HTML. This makes them super fast and great for SEO.

It also means zero* server cost, the server just returns the same file again and again, like a CDN.

@/app/ssg-example/page.tsx
// No fetch, no revalidate, no dynamic params or searchParams. Just plain static content.
export default function Page() {
  return <div>This is a fully static page</div>
}

But what if you still need to fetch some data, like a list of features or testimonials, but that data rarely changes?

You can use cache: "force-cache" in a Server Component. This tells Next.js to fetch the data at build time and reuse the result on every request — just like classic SSG.

@/app/ssg-fetch-example/page.tsx
import { Post } from "@/lib/types";

// A static page that fetches data at build time
export default async function Page() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", { cache: "force-cache" });

  const data = ((await res.json()) as Post[]).slice(0, 10);

  return (
      <ul>
        {data.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
  );
}

✅ This fetch only runs once during the build.
✅ Great for rarely-changing content.
❌ You need to rebuild your app to see updated data.

If you want the same fast speed but want to update the content automatically after a while, check out ISR below 👇

5. Incremental Static Regeneration (ISR)

ISR is one of the best features of Next.js. It gives you the speed of static pages and the ability to update them without rebuilding your whole site.

Normally, if you use SSG (Static Site Generation), your page is built once when you deploy. If something changes in your data, you'd have to rebuild and redeploy your whole app to see the updates.

Here’s how it works:

  • The page is built and cached at build time (just like SSG).
  • You can set a revalidate time, like every 60 seconds.
  • While the cached version is served to users, Next.js checks if it’s time to update.
  • If it's time, the next user who visits the page will trigger a rebuild in the background.
  • That user still sees the old page, but the next one gets the updated version.

So, the page is regenerated quietly while your site stays online and fast.

@/app/isr-example/page.tsx
import { Post } from "@/lib/types";

export default async function Page() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
    next: { revalidate: 60 },
  });

  const data = ((await res.json()) as Post[]).slice(0, 10);

  return (
      <ul>
        {data.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
  );
}

When to use ISR

  • Pages that change sometimes (like product listings, articles, events)
  • You want the performance of static and the freshness of dynamic
  • Great when rebuilding the site every time is too expensive

ISR is what makes scalable content possible, especially if you have hundreds or thousands of pages. You don’t need to rebuild the whole site. Just let Next.js handle it automatically in the background.

6. Partial Prerendering (PPR)

This one’s still experimental, but super interesting.

Imagine your page loads instantly with static HTML for the layout, header, footer, product info, but the personalized parts (like a shopping cart or profile info) or "how many product left?" sections can be loaded afterwards, with a loading spinner.

You can do that with PPR, using <Suspense>, Client Components, and React streaming.

  • The static parts load instantly
  • The dynamic part (ProductList) loads when ready
  • Great for performance + personalization
@/app/ppr-example/page.tsx
// app/ppr-example/page.tsx
import { Suspense } from "react";

export const experimental_ppr = true;

export default function Page() {
  return (
    <>
      <header>
        <h1>This will be prerendered</h1>
      </header>
      <main>
        <Suspense fallback={<div>Loading...</div>}>
          <ProductList />
        </Suspense>
      </main>
    </>
  );
}

// components/ProductList.tsx
import { Post } from "@/lib/types";
export async function ProductList() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
    next: { revalidate: 60 },
  });

  const data = ((await res.json()) as Post[]).slice(0, 10);

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

0

Responses (0)

Sign in to leave a response.

No responses yet. Be the first.

FilmIsBest Təcrübəm

Bu gün sizə ilk layihəm – FilmIsBest haqqında danışmaq istəyirəm. Mən web sahəsini tam sıfırdan 1 il ərzində bu layihənin üzərində işləyərək öyrəndim. Bu müddətdə bir çox çətinliklərlə qarşılaşdım, amma hər biri mənim üçün yeni bir dərs oldu. Bu bloq yazısında sizə həmin problemlərdən, onların həlli yollarından və bu layihənin mənə qazandırdığı təcrübələrdən bəhs edəcəyəm.

3 min read 1 like 0 responses

NextJS üçün Kofe.al Widgeti

Bugün sizlərlə yeni hazırladığım Kofe.al Widget-ni paylaşmaq istəyirəm. NextJS üçün hazırlanmış bu widget həmdə TypeScript ilə yazıldığına görə type-safe-dir. Bu widgeti istədiyiniz kimi istifadə edə bilərsiz. Əslində bir neçə gündür üzərində işlədiyim öz portofolio web-səhifəm üçün hazırlamışdım. Lakin istərəm ki sizlərdə bu komponent-dən istifadə edə biləsiz

2 min read 1 like 0 responses