TechLead

Rendering Strategies

Static, dynamic, streaming, and partial prerendering explained

Rendering in Next.js

Next.js provides multiple rendering strategies to optimize performance and user experience. Understanding when to use each strategy is key to building fast, scalable applications.

🎨 Rendering Strategies

SSG - Static Site Generation (build time)
SSR - Server-Side Rendering (request time)
ISR - Incremental Static Regeneration
CSR - Client-Side Rendering
Streaming - Progressive rendering
PPR - Partial Prerendering (experimental)

Static Site Generation (SSG)

SSG builds HTML at build time, making it fast and cacheable for content that doesn’t change per user.

// Pages are rendered at BUILD time
// Best for: blogs, marketing pages, documentation

// app/posts/page.tsx
export default async function Posts() {
  const posts = await getPosts(); // Fetched at build time
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
}

// Generate static pages for dynamic routes
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({
    slug: post.slug,
  }));
}

export default async function Post({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return <article>{post.content}</article>;
}

// This generates HTML at build time for each post

Server-Side Rendering (SSR)

SSR renders on each request, which is best for personalized or frequently changing data.

// Pages are rendered on EACH REQUEST
// Best for: personalized content, real-time data

// Force dynamic rendering
export const dynamic = 'force-dynamic';

// Or use no-store in fetch
async function getUser() {
  const res = await fetch('/api/user', { cache: 'no-store' });
  return res.json();
}

// Using cookies/headers makes it dynamic automatically
import { cookies, headers } from 'next/headers';

export default async function Dashboard() {
  const cookieStore = await cookies();
  const token = cookieStore.get('auth-token');
  
  const headersList = await headers();
  const userAgent = headersList.get('user-agent');
  
  const user = await getUser(token);
  return <div>Welcome, {user.name}</div>;
}

Incremental Static Regeneration (ISR)

ISR keeps the performance of static pages while allowing periodic or on-demand updates.

// Static pages that revalidate periodically
// Best for: e-commerce, news sites, frequently updated content

// Time-based revalidation
export const revalidate = 60; // Revalidate every 60 seconds

export default async function Products() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }, // Or per-fetch
  }).then(r => r.json());
  
  return <ProductGrid products={products} />;
}

// On-demand revalidation
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';

export async function POST(request: Request) {
  const { path, tag } = await request.json();
  
  if (path) revalidatePath(path);
  if (tag) revalidateTag(tag);
  
  return Response.json({ revalidated: true, now: Date.now() });
}

Streaming with Suspense

Streaming sends parts of the page as soon as they are ready, improving time to first content.

import { Suspense } from 'react';

// Stream content progressively
export default function Page() {
  return (
    <div>
      {/* Renders immediately */}
      <header>
        <h1>Dashboard</h1>
      </header>

      {/* Each section streams independently */}
      <div className="grid grid-cols-3 gap-4">
        <Suspense fallback={<CardSkeleton />}>
          <RevenueCard /> {/* 100ms */}
        </Suspense>
        
        <Suspense fallback={<CardSkeleton />}>
          <UsersCard /> {/* 200ms */}
        </Suspense>
        
        <Suspense fallback={<CardSkeleton />}>
          <OrdersCard /> {/* 500ms */}
        </Suspense>
      </div>

      {/* Slow component doesn't block others */}
      <Suspense fallback={<TableSkeleton />}>
        <DataTable /> {/* 2000ms */}
      </Suspense>
    </div>
  );
}

// Each component can be an async Server Component
async function RevenueCard() {
  const revenue = await getRevenue();
  return <Card>Revenue: {revenue}</Card>;
}

Loading.tsx for Route-level Loading

A loading.tsx file provides a route-level fallback while data and components load.

// app/dashboard/loading.tsx
// Automatically wraps page in Suspense

export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/4 mb-4" />
      <div className="grid grid-cols-3 gap-4">
        <div className="h-32 bg-gray-200 rounded" />
        <div className="h-32 bg-gray-200 rounded" />
        <div className="h-32 bg-gray-200 rounded" />
      </div>
    </div>
  );
}

// File structure
app/
β”œβ”€β”€ dashboard/
β”‚   β”œβ”€β”€ loading.tsx    # Shows while page loads
β”‚   └── page.tsx       # The actual page

Partial Prerendering (PPR)

PPR serves a static shell quickly and streams dynamic sections on request.

// Experimental: Static shell + dynamic holes
// Enable in next.config.js

// next.config.js
const nextConfig = {
  experimental: {
    ppr: true,
  },
};

// app/page.tsx
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      {/* Static shell - prerendered */}
      <Header />
      <Hero />
      
      {/* Dynamic hole - rendered on request */}
      <Suspense fallback={<CartSkeleton />}>
        <Cart /> {/* Uses cookies/auth */}
      </Suspense>
      
      {/* Static content */}
      <FeaturedProducts />
      <Footer />
    </div>
  );
}

// PPR serves static shell instantly from CDN
// Then streams in dynamic parts

πŸ“– Rendering Documentation β†’

Choosing a Strategy

Use Case Strategy Example
Static content SSG Blog, docs
Personalized data SSR Dashboard, settings
Frequently updated ISR E-commerce, news
Mixed static/dynamic PPR Landing + cart
Fast initial load Streaming Complex dashboards

βœ… Rendering Best Practices

  • β€’ Default to static - add dynamism only when needed
  • β€’ Use Suspense boundaries for independent loading
  • β€’ Choose ISR for content that changes periodically
  • β€’ Use streaming for slow data sources
  • β€’ Add loading.tsx for route-level loading states
  • β€’ Consider PPR for pages with mixed static/dynamic content