Menu

7. Rendering in Next.js

Next.js Master Roadmap - IT Technology

This chapter explores the four core rendering strategies in Next.js—CSR, SSR, SSG, and ISR—alongside React Server Components, streaming with Suspense, and partial prerendering. It provides decision matrices, code examples, and performance guidelines to help you choose the right approach for SEO, interactivity, and scalability.

No MCQ questions available for this chapter.

7. Rendering in Next.js

7.1 Rendering Fundamentals

Next.js offers four primary rendering strategies that determine when and how HTML is generated. Choosing the right strategy impacts performance, SEO, and server load.

7.1.1 CSR (Client‑Side Rendering)

In CSR the server sends a minimal HTML shell and a JavaScript bundle. The browser downloads the bundle, then React renders the UI. This pattern requires the use client directive (or a component marked as a Client Component).

  • When to use: Highly interactive dashboards, data‑visualisation tools, or games where SEO is secondary.
  • Trade‑offs: Fast initial shell (low TTFB) but poor SEO because crawlers see little content; higher client‑side CPU usage.

Example:

export default function Dashboard() {
  const [data, setData] = useState();
  useEffect(() => {
    fetchData().then(setData);
  }, []);
  return ;
}

7.1.2 SSR (Server‑Side Rendering)

SSR generates a fresh HTML response on every request. In Next.js, Server Components are SSR by default unless overridden with caching.

  • When to use: Pages that require up‑to‑date data on each request, such as authenticated user feeds or admin panels.
  • Trade‑offs: Excellent SEO and real‑time data, but higher server load and slower TTFB compared to static strategies.

Example:

export default async function Page() {
  const data = await fetch('https://api.example.com/data', { cache: 'no-store' });
  return 
{data.title}
; }

7.1.3 SSG (Static Site Generation)

SSG pre‑renders HTML at build time. The resulting files are served from a CDN, giving the fastest possible TTFB.

  • When to use: Blogs, documentation, marketing sites, or any content that does not change frequently.
  • Trade‑offs: Near‑zero server load and excellent SEO, but data is stale until the next rebuild unless combined with ISR.

Example (generating static params):

export async function generateStaticParams() {
  return posts.map(p => ({ slug: p.slug }));
}

7.1.4 ISR (Incremental Static Regeneration)

ISR blends SSG speed with the ability to update static pages after a configurable interval.

  • Revalidation: Defined by export const revalidate = seconds or via fetch(..., { next: { revalidate: 60 } }).
  • When to use: E‑commerce product listings, news sites, news feeds, or blogs that need near‑real‑time updates without sacrificing CDN caching.
  • Trade‑offs: Slightly higher server load than pure SSG (only on revalidation requests) but still far lower than SSR; SEO remains excellent.

Example:

export const revalidate = 60; // seconds

// Inside a Server Component
export default async function BlogPost({ params }) {
  const res = await fetch(`https://my‑cms.com/posts/${params.slug}`, {
    next: { revalidate: 60 }
  });
  const post = await res.json();
  return 
{post.title}
; }

7.2 React Server Components (RSC)

Next.js 13+ introduces React Server Components, allowing you to split work between server and client while keeping the programming model familiar.

7.2.1 Server Components

Server Components run exclusively on the server, produce zero client‑side JavaScript, and have direct access to backend resources (databases, filesystem, environment variables).

  • Characteristics: Async by default; cannot use React hooks (useState, useEffect), browser APIs (window, document), or event handlers.
  • When to use: Data fetching, markup generation, or any logic that does not require interactivity.

Example:

async function UserProfile({ userId }) {
  const user = await db.user.findUnique({ where: { id: userId } });
  return 
{user.name}
; }

7.2.2 Client Components

Client Components are rendered on the browser after hydration. They can use hooks, state, effects, and browser APIs.

  • Directive: Place 'use client' at the very top of the file, before any imports.
  • When to use: Interactive UI elements such as buttons, forms, modals, or any component that needs useState, useEffect, or event listeners.

Example:

'use client';
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    
  );
}

7.2.3 The 'use client' Directive

The directive acts as a boundary: any file that imports a component marked with 'use client' becomes part of the client bundle. To minimise bundle size, push the directive to leaf components (buttons, inputs, etc.) and keep ancestors as Server Components.

7.3 Streaming & Suspense

Next.js leverages React’s Suspense to stream HTML chunks as Server Components resolve, improving perceived performance.

7.3.1 Suspense

<Suspense> shows a fallback UI while its children are loading. It works naturally with Server Components because they are asynchronous by default.

Example:

<Suspense fallback={}>
  <Comments postId={postId} />
</Suspense>

7.3.2 Streaming UI

When a route contains Suspense boundaries, Next.js streams the HTML in chunks: the shell (layout, headers) is sent first, followed by each suspended segment as its data becomes available.

  • Route‑level: A loading.tsx file in a route segment acts as an automatic Suspense boundary for the entire route.
  • Component‑level: Manual <Suspense> lets you stream specific parts of the UI (e.g., comments, sidebar widgets).
  • Benefit: Improves Time to First Byte (TTFB) and Largest Contentful Paint (LCP) because users see meaningful content sooner.

7.3.3 Partial Prerendering (PPR) – Experimental

PPR (available in Next.js 14+) combines static generation with streaming: the static shell (layout, navigation, footer) is prerendered at build time, while dynamic, user‑specific sections are streamed on request.

  • Configuration: Add export const experimental_ppr = true to the route segment config (layout.tsx or page.tsx).
  • When to use: Dashboards where the sidebar and top‑nav are static but the main feed depends on the logged‑in user.
  • Trade‑offs: Requires opting into an experimental feature; provides SSG‑level TTFB for the shell and SSR‑level freshness for dynamic parts.

Example:

// layout.tsx
export const experimental_ppr = true;

export default function Layout({ children }) {
  return (
    <>
      
      
{children}
); }

Key Formulas & Decision Matrix

Strategy Data Freshness TTFB Server Load SEO Typical Use Case
CSR Real‑time (client) Fast (shell only) Low Poor Dashboards, games, highly interactive tools
SSR Real‑time (per request) Slower (full render) High Excellent Auth pages, user‑specific feeds, admin panels
SSG Build‑time Fastest (CDN) None (static) Excellent Blogs, documentation, marketing sites
ISR Near‑real‑time (after revalidate) Fastest (CDN + revalidation) Low (only on revalidate) Excellent E‑commerce product listings, news, blogs with occasional updates

RSC Decision Rule: Start component as a Server Component. Add 'use client' only when you need any of the following:

  • useState or useReducer
  • useEffect, useLayoutEffect
  • Event handlers (onClick, onSubmit, etc.)
  • Browser‑only APIs (window, document, localStorage)
  • Third‑party libraries that assume a DOM (e.g., charting libraries, map SDKs)

Concrete Examples

  1. Hybrid Page – A product page where server‑side data is passed to an interactive client widget.
    // page.tsx (Server Component)
    export default async function ProductPage({ params }) {
      const product = await fetchProduct(params.id);
      return <ProductClient product={product} />;
    }
    
    // ProductClient.tsx (Client Component)
    'use client';
    import { useState } from 'react';
    export default function ProductClient({ product }) {
      const [qty, setQty] = useState(1);
      return (
        <div>
          <h2>{product.title}</h2>
          <input type="number" value={qty} onChange={e => setQty(Number(e.target.value))} />
          <button>Add to Cart</button>
        </div>
      );
    }
    
  2. Streaming Comments – Show a skeleton while comments load.
    // page.tsx
    import Comments from './comments';
    import CommentSkeleton from './comment-skeleton';
    export default function PostPage({ params }) {
      return (
        <article>
          <h1>Post Title</h1>
          <Suspense fallback={}>
            <Comments postId={params.id} />
          </Suspense>
        </article>
      );
    }
    
  3. ISR Blog – Generate 1 000 post paths at build time, revalidate every hour.
    // blog/[slug]/page.tsx
    export const revalidate = 3600; // 1 hour
    
    export async function generateStaticParams() {
      const posts = await fetch('https://cms.example.com/posts').then(res => res.json());
      return posts.map(p => ({ slug: p.slug }));
    }
    
    export default async function BlogPost({ params }) {
      const res = await fetch(`https://cms.example.com/posts/${params.slug}`, {
        next: { revalidate: 3600 }
      });
      const post = await res.json();
      return <div><h1>{post.title}</h1><p>{post.content}</p></div>;
    }
    
  4. PPR Dashboard – Static navigation, dynamic user feed.
    // layout.tsx
    export const experimental_ppr = true;
    export default function Layout({ children }) {
      return (
        <>
          <nav> {/* static links */} </nav>
          <main>{children}</main>
        </>
      );
    }
    
    // page.tsx (dashboard)
    export default async function Dashboard() {
      const user = await getCurrentUser(); // server‑only
      return (
        <>
          <h1>Welcome, {user.name}!</h1>
          <Suspense fallback={}>
            <UserFeed userId={user.id} />
          </Suspense>
        </>
      );
    }
    

Performance Metrics & Best Practices

  • Target LCP < 2.5 s – SSG, ISR, and PPR typically achieve this because the HTML is delivered quickly from the CDN.
  • Target CLS < 0.1 – Reserve space for placeholders (e.g., min-height on skeletons) to avoid layout shifts when streaming content arrives.
  • Target INP < 200 ms – Keep client‑side JavaScript minimal; move heavy logic to Server Components and use use client only for truly interactive bits.
  • Cache Strategies – Use cache: 'force-cache' (default) for SSG, cache: 'no-store' for SSR, and next: { revalidate: s } or revalidateTag for ISR.
  • Monitoring – Leverage Next.js instrumentation (Web Vitals, Vercel Analytics) to measure TTFB, LCP, CLS, and INP in production.

“Choose the rendering strategy that matches your data freshness needs and traffic pattern. Start static, add revalidation where needed, and only bring client‑side interactivity to the leaves of your component tree.”