← Back to DevBytes

Server-Side Rendering with Turbopack: SSR, SSG, ISR

Server-Side Rendering with Turbopack: SSR, SSG, and ISR Explained

Next.js 15 ships Turbopack as the default bundler for development, and it is increasingly capable in production builds. Turbopack is a Rust-based incremental bundler built by the team behind Webpack, designed to deliver dramatically faster startup and Hot Module Replacement (HMR). When combined with Next.js's rendering model, Turbopack changes how quickly you can iterate on Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR) features.

This tutorial walks through what each rendering strategy means, why Turbopack matters for them, and how to implement all three in a single Next.js App Router project. You will finish with a working understanding of when to reach for each strategy and how to configure them correctly.

What Is Turbopack?

Turbopack is Next.js's successor to Webpack. It performs fine-grained caching at the function level, compiles only what changes, and is written in Rust for single-threaded speed. For SSR workflows, this matters because every route change in development triggers server compilation. With Webpack, large apps could take seconds to compile a new route; Turbopack typically does it in milliseconds.

As of Next.js 15, Turbopack is stable for development and is being rolled out for production builds behind a flag. You can opt in by adding the following to your next.config.ts:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // Enable Turbopack for production builds (when supported by your version)
  // Otherwise it is used automatically in `next dev --turbo`
  experimental: {
    turbopack: {
      rules: {
        "*.svg": {
          loaders: ["@svgr/webpack"],
          as: "*.js",
        },
      },
    },
  },
};

export default nextConfig;

For day-to-day development, simply run next dev --turbo (or rely on the default in Next.js 15+) to get Turbopack-powered SSR compilation.

Why Rendering Strategy Matters

Choosing the right rendering strategy affects three things: time to first byte (TTFB), SEO crawlability, and how stale your content can be. The three core strategies in Next.js are:

With Turbopack, the iteration loop for all three is faster. You can switch a route from SSG to SSR, see the result instantly, and tune caching behavior without long recompilation pauses.

Setting Up the Project

Create a fresh Next.js app with the App Router enabled:

npx create-next-app@latest rendering-demo
cd rendering-demo
npm run dev

Confirm that next dev prints a Turbopack banner. If you are on an older version, run next dev --turbo explicitly. The App Router uses React Server Components by default, which means most of your pages are already server-rendered. The strategies below control when that rendering happens.

Implementing SSG (Static Site Generation)

SSG is the default in the App Router. Any page that does not use dynamic functions like cookies(), headers(), or searchParams is statically rendered at build time. To fetch data at build time, use an async Server Component:

// app/blog/page.tsx
import { notFound } from "next/navigation";

type Post = { id: number; title: string; body: string };

async function getPosts(): Promise<Post[]> {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10", {
    // SSG: cache forever at build time
    cache: "force-cache",
  });
  if (!res.ok) throw new Error("Failed to fetch posts");
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();
  if (posts.length === 0) notFound();

  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <a href={`/blog/${post.id}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </main>
  );
}

For dynamic SSG routes, export generateStaticParams to pre-render known paths at build time:

// app/blog/[id]/page.tsx
type Post = { id: number; title: string; body: string };

async function getPost(id: string): Promise<Post> {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    cache: "force-cache",
  });
  if (!res.ok) return null;
  return res.json();
}

export async function generateStaticParams() {
  const posts = await fetch(
    "https://jsonplaceholder.typicode.com/posts?_limit=10"
  ).then((r) => r.json());

  return posts.map((post: Post) => ({ id: String(post.id) }));
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);
  if (!post) return <p>Not found</p>;

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

After running next build, these pages are emitted as static HTML files. Turbopack compiles each route in parallel during the build, which noticeably reduces total build time on larger apps.

Implementing SSR (Server-Side Rendering)

SSR renders HTML on every request. To opt a route into SSR, either call a dynamic function or set dynamic = "force-dynamic". The cleanest approach is to disable fetch caching for the request:

// app/dashboard/page.tsx
type Stats = { users: number; revenue: number; updatedAt: string };

async function getStats(): Promise<Stats> {
  const res = await fetch("https://api.example.com/stats", {
    // SSR: always fetch fresh data on every request
    cache: "no-store",
  });
  if (!res.ok) throw new Error("Failed to fetch stats");
  return res.json();
}

export const dynamic = "force-dynamic";

export default async function DashboardPage() {
  const stats = await getStats();

  return (
    <main>
      <h1>Dashboard</h1>
      <p>Users: {stats.users}</p>
      <p>Revenue: ${stats.revenue}</p>
      <p>Updated: {stats.updatedAt}</p>
    </main>
  );
}

Use SSR when the page content depends on the authenticated user, on time-sensitive data, or on request-specific inputs like searchParams. Because Turbopack recompiles server components in milliseconds, you can iterate on SSR routes as quickly as client components.

Implementing ISR (Incremental Static Regeneration)

ISR gives you the speed of static HTML with the freshness of server rendering. The page is generated at build time, served instantly, and then regenerated in the background after a configurable time-to-live (TTL). To enable ISR, pass next.revalidate to fetch or export a revalidate value from the page:

// app/news/page.tsx
type Article = { id: number; title: string; url: string };

async function getArticles(): Promise<Article[]> {
  const res = await fetch("https://api.example.com/news", {
    // ISR: regenerate at most once every 60 seconds
    next: { revalidate: 60 },
  });
  if (!res.ok) throw new Error("Failed to fetch news");
  return res.json();
}

// Route-level fallback: revalidate every 60s even without fetch options
export const revalidate = 60;

export default async function NewsPage() {
  const articles = await getArticles();

  return (
    <main>
      <h1>Latest News</h1>
      <ul>
        {articles.map((a) => (
          <li key={a.id}>
            <a href={a.url}>{a.title}</a>
          </li>
        ))}
      </ul>
    </main>
  );
}

For dynamic ISR routes, combine generateStaticParams with revalidate:

// app/news/[slug]/page.tsx
type Article = { slug: string; title: string; body: string };

async function getArticle(slug: string): Promise<Article | null> {
  const res = await fetch(`https://api.example.com/news/${slug}`, {
    next: { revalidate: 60, tags: [`article-${slug}`] },
  });
  if (!res.ok) return null;
  return res.json();
}

export const revalidate = 60;

export async function generateStaticParams() {
  const articles = await fetch("https://api.example.com/news").then((r) =>
    r.json()
  );
  return articles.map((a: Article) => ({ slug: a.slug }));
}

export default async function ArticlePage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const article = await getArticle(slug);
  if (!article) return <p>Article not found</p>;

  return (
    <article>
      <h1>{article.title}</h1>
      <p>{article.body}</p>
    </article>
  );
}

On-Demand Revalidation

Sometimes a TTL is not enough — for example, when an editor publishes a new article and you want it live immediately. Use tag-based revalidation with a route handler:

// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const body = await req.json();
  const secret = req.headers.get("x-revalidate-secret");

  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
  }

  if (body.tag) {
    revalidateTag(body.tag);
    return NextResponse.json({ ok: true, revalidated: body.tag });
  }

  return NextResponse.json({ ok: false, error: "Missing tag" }, { status: 400 });
}

When your CMS publishes an article, send a POST request to /api/revalidate with { "tag": "article-my-slug" } and the secret header. Next.js will regenerate that page on the next request and cache the fresh version.

Best Practices

Conclusion

Turbopack does not change the rendering model of Next.js — SSR, SSG, and ISR work exactly the same way at runtime — but it transforms the developer experience around them. Faster compilation means you can experiment with caching strategies, switch a route from static to dynamic, and tune revalidation intervals without the friction that used to slow teams down. Start with SSG for everything, reach for ISR when content updates periodically, and reserve SSR for truly per-request data. Combined with Turbopack's speed and Next.js's granular caching primitives, you get a rendering pipeline that is both performant for users and pleasant to build with.

— Ad —

Google AdSense will appear here after approval

← Back to all articles