All posts
nextjsssr

Server-Side Rendering in Next.js: A Practical Guide for Full-Stack Developers

A practical guide to Server-Side Rendering in Next.js — setup, core concepts, common mistakes, and production tips for full-stack developers.

SR

Suhail Roushan

August 6, 2026

·
6 min read
·
0 views

Server-Side Rendering in Next.js is the right choice when your page needs fresh data on every request, but it's not the default for every route. This guide covers when to use SSR, how to implement it correctly, and the production pitfalls that bite full-stack developers.

Server-Side Rendering in Next.js means the server fetches data, renders HTML, and sends a complete page to the client on each request. I've seen teams default to SSR for everything, then wonder why their TTFB balloons to 800ms. The App Router changed how we think about this — you're no longer locked into a single rendering strategy per page.

Why Server-Side Rendering in Next.js Matters (and When to Skip It)

SSR exists for one reason: fresh data with zero client-side loading states. If your page shows a user's cart, a live dashboard, or personalized content that changes per request, SSR gives you the best SEO and perceived performance.

But here's my take: most pages don't need SSR. Marketing pages, blog posts, and documentation should use static generation (SSG) or incremental static regeneration (ISR). Static pages serve from a CDN in milliseconds. SSR pages hit your server, query a database, and render — every single time. That's a 10x difference in latency for content that changes weekly, not per-second.

Use SSR when:

  • The data is user-specific and changes frequently
  • The page requires request-time headers or cookies for rendering
  • You're building a dashboard with real-time metrics

Skip SSR when:

  • The content is identical for all users
  • The data updates hourly or daily (use ISR instead)
  • You need to scale with minimal server costs

Getting Started with Server-Side Rendering in Next.js

In the App Router, SSR is the default — but only if you make your component async and fetch data directly. Here's the minimal setup:

// app/dashboard/page.tsx
export default async function DashboardPage() {
  // This fetch runs on the server for every request
  const res = await fetch('https://api.example.com/metrics', {
    cache: 'no-store' // Force SSR — no caching
  });
  const metrics = await res.json();

  return (
    <main>
      <h1>Live Metrics</h1>
      <pre>{JSON.stringify(metrics, null, 2)}</pre>
    </main>
  );
}

The cache: 'no-store' option is the key. Without it, Next.js may cache the response and turn your page into ISR. If you're on the Pages Router, you'd use getServerSideProps:

// pages/checkout.tsx (Pages Router)
export async function getServerSideProps() {
  const res = await fetch('https://api.example.com/pricing');
  const pricing = await res.json();

  return {
    props: { pricing }
  };
}

Core Server-Side Rendering in Next.js Concepts Every Developer Should Know

1. The cache: 'no-store' Directive

This is the single most important concept in the App Router. Without it, your fetch might be cached at the CDN level, making it static. Always pair SSR fetches with no-store:

const res = await fetch('https://api.example.com/user', {
  cache: 'no-store',
  headers: {
    // Pass cookies from the request
    Cookie: cookies().toString()
  }
});

2. Accessing Request Headers

SSR's superpower is request-time data. Use Next's headers() and cookies() helpers:

import { cookies, headers } from 'next/headers';

export default async function ProfilePage() {
  const token = cookies().get('auth_token')?.value;
  const userAgent = headers().get('user-agent');

  const res = await fetch('https://api.example.com/profile', {
    cache: 'no-store',
    headers: { Authorization: `Bearer ${token}` }
  });

  return <div>Profile for {userAgent}</div>;
}

3. Streaming and Suspense

SSR doesn't have to block the whole page. Use Suspense to stream critical sections first:

import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Loading metrics...</div>}>
        <MetricsWidget />
      </Suspense>
    </div>
  );
}

async function MetricsWidget() {
  const data = await fetch('https://api.example.com/metrics', { cache: 'no-store' });
  return <div>{JSON.stringify(await data.json())}</div>;
}

Common Server-Side Rendering in Next.js Mistakes and How to Fix Them

Mistake 1: Forgetting no-store on every fetch. You'll get stale data in production and wonder why your SSR page isn't updating. Fix: make a shared fetch wrapper:

// lib/fetch.ts
export async function ssrFetch(url: string, options?: RequestInit) {
  return fetch(url, {
    ...options,
    cache: 'no-store'
  });
}

Mistake 2: Fetching in client components. If you add async to a client component, Next.js throws an error. The fix is to keep data fetching in server components and pass data down as props, or use useEffect for client-side data fetching (which isn't SSR).

Mistake 3: Blocking the whole page with slow fetches. When one API call takes 3 seconds, the entire page waits. Use Suspense boundaries around independent components, or use Promise.all for parallel fetches:

export default async function Page() {
  const [users, orders, inventory] = await Promise.all([
    fetch('/api/users', { cache: 'no-store' }).then(r => r.json()),
    fetch('/api/orders', { cache: 'no-store' }).then(r => r.json()),
    fetch('/api/inventory', { cache: 'no-store' }).then(r => r.json())
  ]);

  return <Dashboard users={users} orders={orders} inventory={inventory} />;
}

When Should You Use Server-Side Rendering in Next.js?

Use SSR when your page must reflect the current state of data on every request — think authenticated dashboards, admin panels, or e-commerce checkout flows. If a user refreshes and sees stale data, that's a bug. SSR guarantees freshness at the cost of server compute.

Also use SSR when you need request-specific rendering — like showing different content based on cookies, geolocation headers, or A/B test flags. Static pages can't do this without client-side JavaScript, which hurts SEO and initial load.

Avoid SSR for public, identical content. A blog post is the same for everyone — serve it from a CDN with ISR and revalidate every hour. You'll get 50ms response times instead of 500ms.

Server-Side Rendering in Next.js in Production

Tip 1: Cache aggressively at the data layer. SSR doesn't mean every database query is uncached. Use Redis or a query cache for expensive operations that don't change per-request:

import { redis } from './redis';

export async function getMetrics() {
  const cached = await redis.get('metrics');
  if (cached) return JSON.parse(cached);

  const metrics = await db.query('SELECT * FROM metrics');
  await redis.set('metrics', JSON.stringify(metrics), 'EX', 30);
  return metrics;
}

Tip 2: Monitor TTFB (Time to First Byte). If your SSR pages consistently exceed 500ms TTFB, you're doing too much on the server. Profile your route with next build and look at the λ markers — those are dynamic routes.

Tip 3: Consider edge runtime for global users. Next.js supports export const runtime = 'edge' for SSR pages. Edge functions run closer to users, reducing latency. But beware — edge runtime has a smaller API surface. No Node.js fs or heavy libraries.

Tip 4: Set up proper error handling. When your upstream API fails, SSR fails the whole page. Wrap fetches in try/catch and return fallback UI:

export default async function Page() {
  try {
    const data = await fetch('/api/data', { cache: 'no-store' });
    return <DataView data={await data.json()} />;
  } catch {
    return <ErrorFallback />;
  }
}

Here's your actionable takeaway: audit every page in your Next.js app today — if it's using SSR but the content doesn't change per request, convert it to ISR with a sensible revalidate interval. Your server costs will drop, and your users won't notice the difference.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch