Junior 12 min · March 05, 2026

Next.js getServerSideProps — 12k req/s Server Crash

At 12k req/s, average response time jumped from 120ms to 8,400ms, CPU 100%.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Next.js is a React framework that adds file-based routing, server rendering, and build-time optimisation
  • File-based routing: drop a file in pages/ or app/ and the route is live — no router config needed
  • SSG (getStaticProps) builds HTML at deploy time — fastest option, served from CDN
  • SSR (getServerSideProps) builds HTML per request — use only when data must be fresh AND in initial HTML
  • ISR adds a revalidate timer to SSG — static speed with near-real-time freshness
  • Biggest mistake: using SSR everywhere for 'freshness' when ISR solves 90% of those cases faster
✦ Definition~90s read
What is Next.js Basics?

Next.js is React with a default build pipeline that bakes in routing, data fetching, and rendering strategies so you don't have to wire up Webpack yourself. React is a library for building UIs. Next.js is a framework that decides how those UIs get served to a browser.

Think of a regular React app like a food truck where every customer has to wait while the chef cooks their meal from scratch.

The real distinction: React leaves you with a blank HTML file and a JavaScript bundle. Your users download the bundle, React renders the page. That works for dashboards. For public-facing sites where SEO and first paint matter, it's a disaster.

Next.js gives you SSR, SSG, ISR, and client-side rendering out of the box. You pick the strategy per page. It also handles code splitting, image optimization, and API routes. It's not magic — it's opinionated defaults that save you from yourself.

If you're shipping a production app that needs to load fast for real users on poor connections, you don't want to configure Babel and Webpack. You want Next.js. Period.

Plain-English First

Think of a regular React app like a food truck where every customer has to wait while the chef cooks their meal from scratch. Next.js is like a restaurant that pre-cooks popular dishes during quiet hours, so when you sit down, your food arrives almost instantly. For less popular dishes, it still cooks fresh — but it knows the difference. That's the core idea: Next.js decides the smartest way to serve each page of your app.

Next.js solves three problems that plain React cannot: initial page load performance, search engine visibility, and routing complexity. A standard React app ships a blank HTML shell and waits for JavaScript to hydrate — users see a spinner, crawlers see nothing. Next.js renders pages on the server or at build time, so users and search engines get real HTML immediately.

The framework supports four rendering strategies in a single project: Static Site Generation (SSG), Server-Side Rendering (SSR), Client-Side Rendering (CSR), and Incremental Static Regeneration (ISR). Choosing the right strategy per page is the core skill that separates a competent Next.js developer from one who ships slow, expensive applications.

Why getServerSideProps Is Not Your SSR Friend

getServerSideProps is a Next.js function that runs on every request to fetch data at server-side render time. It blocks the response until the async function resolves, meaning the server's event loop is occupied for the duration of the fetch. This is fundamentally different from static generation (getStaticProps) or client-side fetching — it trades caching for freshness, but at a direct cost to request throughput.

Each invocation creates a new Node.js promise chain. If your data source (e.g., a database or external API) has a 200ms latency, a single server process handling 12,000 requests per second will have 2,400 concurrent in-flight promises. Node.js can handle that, but the real bottleneck is the downstream service: a 200ms query under 2,400 concurrent calls often degrades to 2–5 seconds, causing cascading timeouts. The server's connection pool, memory, and CPU all spike linearly with concurrency.

Use getServerSideProps only when you absolutely need per-request data that cannot be cached or pre-rendered — for example, user-specific dashboards behind authentication. For any page where data changes infrequently, prefer Incremental Static Regeneration (ISR) or client-side fetching with a loading state. In high-traffic systems, getServerSideProps is often the first thing to kill your server under load.

The Caching Illusion
getServerSideProps does not cache by default. Every request re-fetches data, even if the response is identical. Add a CDN or edge cache layer, or switch to ISR.
Production Insight
A SaaS dashboard page using getServerSideProps to fetch user data from a Postgres database. Under a marketing campaign spike, the database connection pool exhausted, causing 5-second timeouts and 503 errors for all users. Rule: never use getServerSideProps for database queries that can't complete in under 50ms — use a dedicated API route with connection pooling and caching.
Key Takeaway
getServerSideProps blocks the response — each request ties up a Node.js event loop tick until the data resolves.
Without caching, every request hits your data source — a 200ms fetch under 12k req/s creates 2,400 concurrent calls that degrade downstream services.
Prefer ISR or client-side fetching for any page that doesn't require per-request, user-specific data.
Next.js Data Fetching & Routing Tradeoffs THECODEFORGE.IO Next.js Data Fetching & Routing Tradeoffs Comparing getServerSideProps, SSG, CSR, and App Router getServerSideProps Server-side render per request, 12k req/s crash File-Based Routing Folder structure defines routes, not config SSG vs SSR vs CSR Static generation, server render, client fetch App Router vs Pages Router New App Router with React Server Components API Routes & Middleware Backend logic inside Next.js Deployment & Performance Optimize for production, avoid SSR pitfalls ⚠ getServerSideProps blocks rendering until data fetch completes Use SSG or ISR for static content; App Router with streaming THECODEFORGE.IO
thecodeforge.io
Next.js Data Fetching & Routing Tradeoffs
Nextjs Basics

File-Based Routing: Why Your Folder Structure IS Your App's Navigation

In a standard React app, you manually wire up React Router — you import BrowserRouter, define Route components, and manage path strings in your head. It works, but it's boilerplate you didn't need to write. Next.js takes a different philosophy: your file system is your router. Drop a file called about.js inside the pages folder and /about becomes a live route instantly. No imports. No configuration. No forgetting to register a new page.

This isn't just a convenience trick. It's an architectural choice that enforces consistency across teams. When a new developer joins your project, they don't need to read routing config — they just look at the folder structure. Files are truth.

Dynamic routes use square-bracket syntax. A file named [productId].js matches /products/42, /products/shoes, or any slug you throw at it. The matched value lands in useRouter().query so you can fetch the right data. This pattern covers 90% of real-world routing needs — product pages, blog posts, user profiles — without a single line of router config.

ProductPage.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// File location: pages/products/[productId].js
// This single file handles EVERY product URL: /products/1, /products/42, etc.

import { useRouter } from 'next/router';

export default function ProductPage() {
  // useRouter gives us access to the URL parameters
  const router = useRouter();

  // productId comes from the filename [productId].js
  // On the URL /products/42, this will be the string '42'
  const { productId } = router.query;

  // During the initial render on the client, query can be empty
  // so we guard against that with a loading state
  if (!productId) {
    return <p>Loading product...</p>;
  }

  return (
    <div>
      <h1>Product Details</h1>
      {/* Shows the dynamic segment from the URL */}
      <p>You are viewing product ID: {productId}</p>
      <p>
        This page was generated by the file:
        pages/products/[productId].js
      </p>
    </div>
  );
}

// --- FOLDER STRUCTURE REFERENCE ---
// pages/
//   index.js          → renders at /
//   about.js          → renders at /about
//   products/
//     index.js        → renders at /products
//     [productId].js  → renders at /products/:productId
//   blog/
//     [slug].js       → renders at /blog/:slug
Watch Out: router.query is empty on first render
When Next.js hydrates a page on the client, router.query starts as an empty object {} before populating with the actual URL values. Always guard with if (!productId) return null or a loading state — otherwise you'll pass undefined to your API calls and wonder why nothing works.
Production Insight
router.query is empty during hydration — the first client render has no URL data.
API calls using router.query during the first render send undefined as the ID.
Rule: guard router.query with a loading state — never use it unconditionally.
Key Takeaway
File-based routing means your folder structure IS your navigation — no router config to maintain.
Dynamic routes use [param].js syntax — the matched value lands in router.query.
router.query is empty on first render — always guard with a loading state.

SSG vs SSR vs CSR: Choosing the Right Data Fetching Strategy

This is where Next.js earns its keep — and where most developers get confused because they treat all three strategies as interchangeable. They're not. Each one answers a different question about WHEN data should be fetched and WHO should do the fetching.

Static Site Generation (SSG) with getStaticProps runs at build time on the server. The HTML is generated once and served from a CDN forever. This is perfect for content that doesn't change often — a product catalogue, a blog post, a pricing page. It's the fastest option because there's no server work happening per request.

Server-Side Rendering (SSR) with getServerSideProps runs on every single request. The server fetches fresh data and builds the HTML before sending it to the user. Use this only when the data must be up-to-the-second fresh AND you need it in the initial HTML — think a live dashboard or a personalised feed that depends on the user's session cookie.

Client-Side Rendering (CSR) is just regular React — the page loads, then JavaScript fetches data in a useEffect. Use this for data that's behind authentication and doesn't need to be indexed by search engines, like user account settings. Mixing all three in the same app based on each page's needs is the Next.js superpower.

BlogPost.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// File: pages/blog/[slug].js
// This page uses SSG — it's pre-built at deploy time
// Perfect for blog posts that don't change every minute

export default function BlogPost({ post }) {
  // 'post' comes from getStaticProps below — it's already resolved
  // by the time this component renders. No loading state needed!
  return (
    <article>
      <h1>{post.title}</h1>
      <p>By {post.author} on {post.publishedDate}</p>
      <div>{post.content}</div>
    </article>
  );
}

// getStaticPaths tells Next.js WHICH URLs to pre-build
// This runs at build time — not on every request
export async function getStaticPaths() {
  // In real life, fetch this list from your CMS or database
  const publishedSlugs = ['intro-to-nextjs', 'react-hooks-explained', 'css-grid-guide'];

  const paths = publishedSlugs.map((slug) => ({
    params: { slug }, // must match the [slug] in your filename
  }));

  return {
    paths,
    // fallback: false means unknown slugs return a 404
    // fallback: 'blocking' means unknown slugs trigger SSR on first hit, then cache
    fallback: false,
  };
}

// getStaticProps receives the slug and fetches that post's data
// Runs ONCE per slug at build time — never again (until revalidate triggers)
export async function getStaticProps({ params }) {
  // Simulating a CMS or database fetch using the slug from the URL
  const response = await fetch(`https://api.example.com/posts/${params.slug}`);
  const post = await response.json();

  return {
    props: { post }, // injected as props into BlogPost component above
    // revalidate: 60 means re-generate this page in the background
    // if a request comes in after 60 seconds — this is called ISR
    revalidate: 60,
  };
}

// --- WHEN TO USE WHAT ---
// getStaticProps    → blog posts, product pages, marketing pages (fast, SEO-friendly)
// getServerSideProps → user dashboards, pages needing live data or session cookies
// useEffect fetch   → post-login UI, private account pages, non-SEO content
Pro Tip: ISR is the sweet spot for most real apps
Incremental Static Regeneration (ISR) — enabled by adding revalidate: N to your getStaticProps return — gives you static speed with near-real-time freshness. The page serves from cache until N seconds pass, then regenerates in the background on the next visit. Most e-commerce and content sites should default to ISR over full SSR.
Production Insight
getServerSideProps runs a server function on every request — the most expensive strategy.
ISR with revalidate: N serves cached pages and regenerates in the background.
Rule: default to SSG/ISR — only use SSR when data must be fresh per-request AND in initial HTML.
Key Takeaway
SSG builds HTML at deploy time — fastest, served from CDN, no server work per request.
SSR builds HTML per request — expensive, use only when data must be fresh and in initial HTML.
ISR is the sweet spot — static speed with near-real-time freshness via revalidate timer.

Two components trip up almost every developer coming from plain React: next/link and next/image. They look like simple wrappers around an anchor tag and an img tag, but they're doing serious work under the hood — work that would take hours to implement manually.

next/link enables client-side navigation. When you use it, clicking a link doesn't trigger a full browser reload — Next.js swaps the page in JavaScript, keeping your app state alive. But here's the part that actually matters: Next.js automatically prefetches linked pages in the background when they appear in the viewport. The user moves their mouse toward a link and the destination is already loading. This is why Next.js apps feel snappy — it's not magic, it's strategic prefetching.

next/image solves one of the biggest real-world performance problems: images. It automatically resizes images to the right dimensions for the user's device, serves modern formats like WebP when the browser supports it, lazy-loads images below the fold by default, and prevents layout shift by requiring width and height props. The priority prop tells it to load a specific image eagerly — use that on your above-the-fold hero image to nail your Core Web Vitals score.

ProductCard.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// File: components/ProductCard.js
// Demonstrates real-world usage of next/link and next/image

import Link from 'next/link';
import Image from 'next/image';

export default function ProductCard({ product }) {
  return (
    <div className="product-card">
      {/*
        next/image vs plain <img>:
        - Automatically serves WebP format if browser supports it
        - Resizes to the rendered size — not the original 4000px source
        - Lazy loads by default (doesn't load until near viewport)
        - Prevents CLS (Cumulative Layout Shift) via the width/height aspect ratio
      */}
      <Image
        src={product.imageUrl}
        alt={product.name}       // alt is required — good for accessibility AND SEO
        width={400}              // intrinsic width of the image
        height={300}             // intrinsic height — sets the aspect ratio
        // Add priority={true} ONLY for above-the-fold images like hero banners
        // priority={true} tells Next.js to preload it, skipping lazy loading
      />

      <h2>{product.name}</h2>
      <p>${product.price}</p>

      {/*
        next/link vs plain <a>:
        - Client-side navigation — no full page reload
        - Prefetches the linked page HTML+JS in the background when visible
        - Falls back to normal <a> behaviour if JavaScript hasn't loaded yet
      */}
      <Link href={`/products/${product.id}`}>
        {/* As of Next.js 13+, you don't need a nested <a> tag anymore */}
        View Product Details
      </Link>
    </div>
  );
}

// Usage in a parent page:
// <ProductCard
//   product={{
//     id: 42,
//     name: 'Trail Running Shoes',
//     price: 129.99,
//     imageUrl: '/images/trail-shoes.jpg'
//   }}
// />
Interview Gold: Why does next/image require width and height?
It's not just about layout — it's about Cumulative Layout Shift (CLS), a Core Web Vitals metric. Without explicit dimensions, the browser doesn't know how much space to reserve for an image before it loads, causing content to jump around as images pop in. Providing width and height lets the browser pre-allocate that space, which keeps your CLS score near zero and directly impacts your Google search ranking.
Production Insight
next/image prevents CLS by reserving space before the image loads — width/height are required.
Without dimensions, images cause layout shift — Google penalises CLS in search rankings.
Rule: always provide width and height to next/image — it is a Core Web Vitals requirement.
Key Takeaway
next/link prefetches pages in the background when they enter the viewport — this is why Next.js feels fast.
next/image serves WebP, resizes automatically, and prevents CLS via required width/height.
Use priority={true} only for above-the-fold images — it skips lazy loading for critical content.

The App Router vs Pages Router: What Changed in Next.js 13+

If you've been reading Next.js docs and feeling confused by two completely different patterns, here's the honest explanation: Next.js 13 introduced a brand-new routing system called the App Router, and both systems now coexist in the same framework. The older system is the Pages Router (everything inside a pages/ folder, which is what this article has covered so far). The App Router lives inside an app/ folder and uses a different set of conventions.

The App Router is built around React Server Components — a newer React feature that lets components run only on the server, with zero JavaScript shipped to the browser. In the App Router, every component is a Server Component by default. You opt into client-side interactivity by adding 'use client' at the top of a file.

The data fetching story also changes: instead of getStaticProps and getServerSideProps, you just use async/await directly inside Server Components and call fetch() with caching options. It's more flexible but requires a mental shift.

For new production projects starting today, learn the App Router. For jobs and existing codebases, the Pages Router is still everywhere — understanding both is what separates a competent Next.js developer from a great one.

ProductPageAppRouter.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// File: app/products/[productId]/page.js  (App Router convention)
// Notice: no getStaticProps, no useRouter for data — just async/await directly

// This is a SERVER COMPONENT by default.
// It runs only on the server. Zero JS is sent to the browser for this component.
// That means: faster page loads, direct database access, no API layer needed.

export default async function ProductPage({ params }) {
  // params comes from the folder name [productId] — same concept as Pages Router
  const { productId } = params;

  // fetch() in Server Components is enhanced by Next.js
  // next: { revalidate: 3600 } is equivalent to ISR — cache for 1 hour
  const response = await fetch(
    `https://api.example.com/products/${productId}`,
    { next: { revalidate: 3600 } } // ISR-style caching built right into fetch
  );

  const product = await response.json();

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>Price: ${product.price}</p>

      {/*
        AddToCartButton needs onClick — that's client-side behaviour.
        We mark it 'use client' in its own file and import it here.
        The Server Component passes data down; the Client Component handles interaction.
      */}
      <AddToCartButton productId={productId} />
    </div>
  );
}

// --- app/components/AddToCartButton.js ---
// 'use client' at the top opts this file into client-side rendering
// Only components that NEED interactivity should be client components

'use client';

import { useState } from 'react';

export function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false);

  function handleClick() {
    // In real life: call your cart API here
    console.log(`Adding product ${productId} to cart`);
    setAdded(true);
  }

  return (
    <button onClick={handleClick}>
      {added ? 'Added to Cart ✓' : 'Add to Cart'}
    </button>
  );
}
Pro Tip: Don't reach for 'use client' by default
New App Router developers instinctively add 'use client' to every component because it feels familiar. Resist this. Keep components as Server Components unless they specifically need useState, useEffect, browser APIs, or event handlers. The more Server Components you have, the less JavaScript your users download — and that's the whole point of the App Router.
Production Insight
Adding 'use client' to every component defeats the App Router purpose — you ship more JavaScript.
Server Components run only on the server — zero JS shipped for those components.
Rule: only add 'use client' when a component needs useState, useEffect, or event handlers.
Key Takeaway
App Router uses Server Components by default — zero JS shipped for server-only components.
Data fetching uses async/await directly in components — no getStaticProps or getServerSideProps.
Only add 'use client' when a component needs browser interactivity — resist the instinct to add it everywhere.

API Routes and Middleware: Building the Backend Inside Next.js

Next.js includes a built-in API layer. Files inside pages/api/ become serverless API endpoints — no separate backend server needed. This is the simplest way to add server-side logic to a Next.js application: form submissions, webhook handlers, authentication endpoints, and database queries all live inside the same project.

The App Router changes the API story. Instead of pages/api/, API routes live inside app/api/ as route.js files. Each HTTP method (GET, POST, PUT, DELETE) is exported as a named function. The handler receives a standard Web Request object and returns a standard Web Response — aligning with the web platform instead of Node.js-specific APIs.

Middleware runs before every request reaches your pages or API routes. It lives in middleware.js at the project root and is the right place for authentication checks, redirects, feature flags, and request logging. Middleware runs on the Edge Runtime by default — it executes at the CDN edge, not on your origin server, which means sub-millisecond latency for simple checks.

route.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// --- App Router API Route ---
// File: app/api/users/route.js
// Each HTTP method is a named export

import { NextResponse } from 'next/server';

// GET /api/users — list all users
export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');
  const limit = parseInt(searchParams.get('limit') || '20');

  // In production: call your database here
  const users = await db.users.findMany({
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({
    data: users,
    pagination: { page, limit },
  });
}

// POST /api/users — create a new user
export async function POST(request) {
  const body = await request.json();

  // Validate input
  if (!body.email || !body.name) {
    return NextResponse.json(
      { error: 'Email and name are required' },
      { status: 400 }
    );
  }

  const user = await db.users.create({ data: body });

  return NextResponse.json({ data: user }, { status: 201 });
}

// --- Middleware ---
// File: middleware.js (project root)
// Runs on every request before pages/API routes

import { NextResponse } from 'next/server';

export function middleware(request) {
  // Example: redirect unauthenticated users
  const token = request.cookies.get('auth-token');

  // Protect dashboard routes
  if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // Add request ID for tracing
  const response = NextResponse.next();
  response.headers.set('x-request-id', crypto.randomUUID());

  return response;
}

// Matcher controls which routes trigger middleware
export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};
Middleware as a Gatekeeper
  • Runs on the Edge Runtime — executes at the CDN, not your origin server
  • Sub-millisecond latency for simple checks like auth tokens and feature flags
  • Cannot use Node.js APIs (fs, crypto.randomBytes) — only Web APIs available
  • Use matcher config to limit which routes trigger middleware — avoid running on static assets
  • The right place for: auth checks, redirects, A/B testing, request logging, rate limiting
Production Insight
Middleware runs on the Edge Runtime — no Node.js APIs available (no fs, no crypto.randomBytes).
Middleware on every request adds latency — use matcher to limit which routes trigger it.
Rule: use middleware for auth and redirects — put business logic in API routes.
Key Takeaway
API routes turn Next.js into a full-stack framework — no separate backend needed.
App Router API routes use named exports (GET, POST) with standard Web Request/Response.
Middleware runs at the CDN edge — sub-millisecond latency for auth checks and redirects.

Deployment and Performance: What Matters in Production

Understanding rendering strategies is only half the picture. In production, deployment configuration, caching headers, and build optimisation determine whether your app is fast or expensive. A perfectly coded Next.js app deployed without CDN caching is slower than a poorly coded app deployed with proper caching.

Vercel is the default deployment platform for Next.js — both are made by the same company. It handles ISR caching, edge functions, image optimisation, and preview deployments automatically. But Next.js deploys anywhere: AWS, Google Cloud, Docker containers, or static hosting for fully-static sites.

The three production levers: CDN caching (serve static assets from edge locations), ISR revalidation (serve cached pages, regenerate in background), and bundle analysis (ship less JavaScript to the browser). Pulling these three levers correctly is worth more than any code optimisation.

next.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// File: next.config.js
// Production configuration that actually matters

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Image optimisation domains — required for next/image with remote images
  images: {
    domains: ['images.example.com', 'cdn.example.com'],
    // Or use remotePatterns for more control (Next.js 13+)
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.example.com',
      },
    ],
  },

  // Headers: set caching policies for static assets
  async headers() {
    return [
      {
        // Cache static assets (JS, CSS, images) for 1 year
        source: '/_next/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        // Cache API responses for 60 seconds
        source: '/api/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=60, stale-while-revalidate=300',
          },
        ],
      },
    ];
  },

  // Redirects: handle legacy URLs and SEO
  async redirects() {
    return [
      {
        source: '/old-blog/:slug',
        destination: '/blog/:slug',
        permanent: true, // 301 redirect — SEO-friendly
      },
    ];
  },

  // Rewrites: proxy external APIs through Next.js
  async rewrites() {
    return [
      {
        source: '/api/external/:path*',
        destination: 'https://external-api.example.com/:path*',
      },
    ];
  },

  // Bundle analysis — run ANALYZE=true next build to visualise
  // Identifies large dependencies that should be code-split
  ...(process.env.ANALYZE === 'true' && {
    webpack: (config) => {
      const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
      config.plugins.push(
        new BundleAnalyzerPlugin({
          analyzerMode: 'static',
          reportFilename: './bundle-analysis.html',
        })
      );
      return config;
    },
  }),
};

module.exports = nextConfig;

// --- Production Checklist ---
//
// 1. Run ANALYZE=true next build — check bundle-analysis.html for large deps
// 2. Set Cache-Control headers for static assets (1 year, immutable)
// 3. Set Cache-Control for API routes (s-maxage + stale-while-revalidate)
// 4. Use next/image for all images — never use plain <img>
// 5. Use next/link for all internal navigation — never use plain <a>
// 6. Pre-render top pages with getStaticProps — serve from CDN
// 7. Use ISR for pages that change infrequently — not full SSR
// 8. Add middleware for auth — do not check auth in every page component
The Three Most Expensive Mistakes in Production
  • No Cache-Control headers — every request hits your server, even for unchanged static assets
  • Using SSR (getServerSideProps) on pages that could use ISR — paying for server compute on every request
  • Not using next/image — shipping unoptimised 4MB images destroys LCP and bandwidth costs
Production Insight
No Cache-Control headers means every request hits your server — even for unchanged assets.
ISR serves cached pages and regenerates in background — 95% reduction in server compute.
Rule: set Cache-Control headers, use ISR over SSR, and optimise images with next/image.
Key Takeaway
Deployment configuration matters as much as code quality — caching headers and ISR are the biggest levers.
Cache-Control with s-maxage and stale-while-revalidate reduces origin load dramatically.
Bundle analysis reveals oversized dependencies — run ANALYZE=true next build to visualise.

What Is Next.js? (The One Sentence That Actually Matters)

Next.js is React with a default build pipeline that bakes in routing, data fetching, and rendering strategies so you don't have to wire up Webpack yourself. React is a library for building UIs. Next.js is a framework that decides how those UIs get served to a browser.

The real distinction: React leaves you with a blank HTML file and a JavaScript bundle. Your users download the bundle, React renders the page. That works for dashboards. For public-facing sites where SEO and first paint matter, it's a disaster.

Next.js gives you SSR, SSG, ISR, and client-side rendering out of the box. You pick the strategy per page. It also handles code splitting, image optimization, and API routes. It's not magic — it's opinionated defaults that save you from yourself.

If you're shipping a production app that needs to load fast for real users on poor connections, you don't want to configure Babel and Webpack. You want Next.js. Period.

WhyNext.js.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — javascript tutorial

// Without Next.js: manual SSR setup
const express = require('express');
const React = require('react');
const { renderToString } = require('react-dom/server');

const app = express();
app.get('/', (req, res) => {
  const html = renderToString(React.createElement('h1', null, 'Hello'));
  res.send(`<html><body>${html}</body></html>`);
});

// With Next.js: app/page.js handles this automatically
export default function Page() {
  return <h1>Hello</h1>;
}
Output
No output — the framework abstracts the SSR plumbing away.
Senior Shortcut:
If you're debating React vs Next.js for a new project, ask: 'Will this page need SEO or fast initial paint?' If yes, pick Next.js. If it's an internal tool behind a login, plain React is fine.
Key Takeaway
Next.js is not 'React plus extra stuff' — it's a framework that decides how your React components reach the browser.

Why To Use Next.js? (Because Default React Setup Is a Trap)

Default React apps suffer from three fatal flaws in production. First, the whole page is locked inside a single JavaScript bundle. On slow networks, that bundle takes forever to load. Users stare at a blank screen. Second, search engines don't execute JavaScript reliably. Your beautifully designed page gets indexed as empty divs. Third, routing isn't built in. You're forced to install react-router, configure it, and manage fallbacks yourself.

Next.js solves all three without you writing boilerplate. File-based routing means adding a file to the pages or app directory creates a route instantly. No router config. No fallback handling. The framework pre-renders pages at build time or request time, so search engines see real HTML. Code splitting happens per page automatically — users only download what they need.

You also get API routes inside the same project. Need a backend for form handling or auth? Add a file under pages/api. Done. No separate server. No CORS headaches.

The tradeoff is small: you surrender some control over build tooling. In return, you eliminate a mountain of setup that breaks constantly in production. If you've ever debugged a Webpack config at 2 AM, you already understand why that trade is worth it.

ProductionSetup.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — javascript tutorial

// Bad: Manual React setup with CRA, then adding SSR later
// npx create-react-app my-app  // Choose this for internal tools
// npm install react-router-dom
// npm install express react-dom/server

// Good: Next.js setup from the start
// npx create-next-app@latest my-app  // Choose this for public-facing apps
// File structure creates routes
// pages/index.js -> localhost:3000/
// pages/about.js -> localhost:3000/about
Output
Running 'npx create-next-app@latest' scaffolds a production-ready project in 30 seconds.
Production Trap:
Don't add Next.js to an existing CRA or Vite project. It's not a library you import — it's a full framework replacement. The pain of migrating an existing SPA to Next.js is real. Start with it for new projects.
Key Takeaway
Use Next.js when your app needs SEO, faster load times, or a built-in backend — avoid it if you're building a simple internal dashboard.

Key Features of Next.js (The Ones That Actually Move The Needle)

Next.js has a long feature list. Most of it is marketing fluff. These four features actually matter in production:

  1. File-based routing. Your folder structure becomes your URL structure. No router config files. No ambiguous path matching. Add a file, get a route. It's that simple. The App Router also supports layouts, loading states, and error boundaries at every route level.
  2. Hybrid rendering. You can mix SSR, SSG, ISR, and CSR in the same app. A marketing page can be statically generated at build time. A dashboard can use client-side rendering. A product page with frequent updates can use ISR to revalidate every 60 seconds. You decide per page, not per app.
  3. Automatic code splitting. Next.js splits your JavaScript by route. When a user visits /about, they download only the code for that page. The rest of the bundle stays on the server. This is the single biggest performance win over traditional SPAs.
  4. Built-in image optimization. The Image component automatically serves responsive images, lazy loads them, optimizes format, and prevents layout shift. It seems minor until your Lighthouse score jumps from 60 to 95 just by switching from <img> to <Image>.

Everything else — middleware, API routes, static exports — is convenient but secondary. These four features are why Next.js dominates production React.

HybridRendering.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — javascript tutorial

// app/page.js — Statically generated at build time
export default function Home() {
  return <h1>Marketing page — SSG</h1>;
}

// app/dashboard/page.js — Client-side rendering
export default function Dashboard() {
  // No async data fetching — loads in browser
  return <h1>DashboardCSR</h1>;
}

// app/products/[id]/page.js — ISR: revalidate every 60 seconds
export default async function Product({ params }) {
  const data = await fetch(`https://api.example.com/products/${params.id}`, { next: { revalidate: 60 } });
  return <h1>{data.name}</h1>;
}
Output
Home page loads instantly from CDN cache. Dashboard loads after client JS executes. Product page serves stale data for up to 60s, then regenerates.
Production Trap:
Don't default to SSR for every page. If the content doesn't change per user (e.g., blog, docs, landing page), use SSG or ISR. SSR adds server load and latency. Only use it when you need per-request data (e.g., user-specific dashboards).
Key Takeaway
The killer feature of Next.js is per-page rendering control — not SSR for everything, but the right rendering strategy for each route.

CSS Modules: Scoped Styles Without the Framework Headache

CSS Modules solve the global scope problem by generating unique class names at build time. No naming conventions, no BEM—just import a .module.css file and use it like an object. The why: traditional CSS cascades globally, so a class like 'button' in one component can break another. CSS Modules scope styles to the component by appending a hash to the class name. In Next.js, this works out of the box with both Pages and App Router—no configuration needed. The performance win: only the CSS used by a rendered component is included in the bundle. The trap: you cannot target child elements with regular CSS selectors inside a module. Instead, compose classes using the composes keyword or nest via the :global pseudo-selector. This approach is ideal for component libraries or any project where style isolation matters more than runtime CSS-in-JS overhead.

Button.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — javascript tutorial

import styles from './Button.module.css';

export default function Button({ variant, children }) {
  // Composes class based on prop, scoped automatically
  const className = `${styles.btn} ${styles[variant] || ''}`;
  return <button className={className}>{children}</button>;
}

// Button.module.css
// .btn { padding: 8px 16px; border: none; }
// .primary { background: blue; color: white; }
Output
Renders <button class="Button_btn_abc123 Button_primary_def456">Click</button>
Production Trap:
CSS Modules do NOT support dynamic class composition at runtime. If you concatenate class names from multiple modules, you'll break scoping. Use a utility like clsx with static module references.
Key Takeaway
CSS Modules give you component-level style isolation with zero runtime cost—but all class references must be static.

Next.js Basics 1: Installation and Project Scaffolding

Starting a Next.js project is a single command that sets up TypeScript, ESLint, Tailwind CSS, the App Router, and a src/ directory—all opt-in via prompts. The why: skipping the manual Webpack or Vite setup avoids configuration drift between environments. Run npx create-next-app@latest my-app and answer four questions: TypeScript (yes), ESLint (yes), Tailwind (no unless you need it), src/ directory (yes), App Router (yes), custom import alias (default). The result is a zero-config project with build optimization, image handling, and route segments ready. The performance insight: using src/ keeps your app logic separate from public/ assets and next.config.js. The common mistake: forgetting to run npm run dev instead of npm start during development—next start runs the production build. Test by visiting http://localhost:3000—you'll see the default Next.js page confirming the App Router is live.

Terminal.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — javascript tutorial

// Step 1: Create the project
npx create-next-app@latest my-app --typescript --eslint --src-dir --app

// Step 2: Enter directory and start dev server
cd my-app
npm run dev

// Step 3: Build for production
npm run build
export NODE_ENV=production && npm start
Output
✓ Ready on http://localhost:3000
(Next.js 14.2.x with App Router activated)
Production Trap:
Running npm run dev in production is a security risk—it exposes React error overlays and debugging info. Always use npm run build then npm start in deployment.
Key Takeaway
A single command scaffolds an optimized project—choose App Router and src/ directory from the prompts to avoid migration pain later.

How to Integrate Your Favorite Tools

Next.js acts as a blank canvas, but real projects demand specific tooling. The golden rule is to integrate at the right layer. For state management (Redux, Zustand), wrap your app in providers.js inside the app directory using a client component. For styling (Tailwind, Styled Components), install the library and configure tailwind.config.js or use a _document.js for global CSS-in-JS. For data fetching libraries (React Query, SWR), create a custom hook that calls your API routes — this keeps caching logic inside Next.js rather than duplicating it. For authentication (NextAuth, Clerk), use middleware to protect routes at the edge, not inside every page component. The key insight: never fight Next.js's file-based routing. If a tool needs global context, instantiate it in layout.js. If it's page-specific, keep it local. This prevents hydration mismatches and keeps your bundle lean.

Providers.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — javascript tutorial
// Wrap Zustand store in a client component
'use client';
import { create } from 'zustand';

export const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

export default function Providers({ children }) {
  // Zustand doesn't need a provider — this file is for future tools
  return <>{children}</>;
}
Output
No output — this is a wrapper pattern for server components.
Production Trap:
Don't put 'use client' on your root layout unless necessary. It turns your entire app into client-side rendering, killing SSG benefits.
Key Takeaway
Integrate tools at the smallest possible scope — never the root layout unless the tool requires global context.

Practice Code Examples

Theory without code is just philosophy. Here's a practical challenge: build a real-time search filter that fetches from an API route and displays results with debouncing. The solution combines three Next.js patterns: a server-side API route for data, a client component for interactivity, and a custom hook for debouncing. First, create app/api/search/route.js to accept query parameters and return JSON. Then build a SearchBox client component with useState and useEffect. Import a debounce function (or write a 5-line one using setTimeout). Store the debounced value in state, then fetch from your API route using fetch('/api/search?q=' + debouncedQuery). Display results in a list. For bonus points, add loading and error states. This pattern mirrors real-world autocomplete, which is the most common Next.js interview question. The key principle: let the server do the heavy lifting, and keep the client lean.

SearchBox.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — javascript tutorial
'use client';
import { useState, useEffect } from 'react';

export default function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const timer = setTimeout(async () => {
      const res = await fetch(`/api/search?q=${query}`);
      const data = await res.json();
      setResults(data);
    }, 400);
    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
      <ul>{results.map((r) => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
}
Output
Renders a search input that fetches results from /api/search?q=... after 400ms of inactivity.
Production Trap:
Never debounce with a library dependency. A 5-line setTimeout works and keeps your bundle size under 100KB.
Key Takeaway
Real-time search is the #1 Next.js interview pattern — master this 30-line component and you've solved 80% of frontend challenges.
● Production incidentPOST-MORTEMseverity: high

getServerSideProps on Every Page Crashed the Server Under Black Friday Traffic

Symptom
Average response time jumped from 120ms to 8,400 CPU hit 100% and started dropping connections. Users saw timeouts and 502 errors. Revenue dropped 40% in the first hour.
Assumption
getServerSideProps was used on product pages because the team wanted 'fresh' pricing data on every request.
Root cause
Every product page used getServerSideProps to fetch pricing from the database on every request. Product prices changed once per day at most. The team chose SSR because they wanted to avoid stale pricing, but ISR with a 60-second revalidate would have served the same freshness with 99% less server load. Under normal traffic (500 req/s), the server handled SSR fine. Under Black Friday traffic (12,000 req/s), the server could not generate 12,000 HTML pages per second — each page took 50-200ms to render, and the server had 4 cores.
Fix
Converted product pages from getServerSideProps to getStaticProps with revalidate: 60. Pre-built the top 10,000 products at deploy time. Less popular products used fallback: 'blocking' to SSR on first hit, then cache. Added an on-demand revalidation webhook that triggers when prices change in the CMS. Response time dropped to 15ms for cached pages. Server CPU dropped to 15% under the same traffic.
Key lesson
  • getServerSideProps runs a server function on every request — it is the most expensive rendering strategy
  • ISR with revalidate: N serves cached pages and regenerates in the background — use it instead of SSR for data that changes infrequently
  • Pre-build your most popular pages at deploy time — they serve instantly from CDN
  • Use on-demand revalidation (res.revalidate) to invalidate specific pages when data changes — do not rely on time-based revalidation alone
Production debug guideDiagnose rendering and data fetching issues in production5 entries
Symptom · 01
Page loads slowly despite using getStaticProps
Fix
Check if the page is actually being served statically — run next build and verify the page appears as ● (static) not λms during peak traffic. The server's (server)
Symptom · 02
router.query is empty on first render
Fix
This is expected — router.query is empty during hydration. Add a loading state or use getServerSideProps to pass the query as props
Symptom · 03
ISR page shows stale data after CMS update
Fix
Use on-demand revalidation with res.revalidate('/path') triggered by a CMS webhook — do not rely on time-based revalidation alone
Symptom · 04
getStaticPaths with fallback: false returns 404 for valid pages
Fix
Set fallback: 'blocking' to SSR unknown pages on first hit, then cache them — or pre-build all valid paths
Symptom · 05
App Router Server Component throws error when using useState
Fix
Server Components cannot use React hooks — add 'use client' directive to the file or move the interactive part to a separate client component
★ Next.js Quick Debug ReferenceFast commands for diagnosing Next.js rendering and build issues
Page is server-rendered instead of static
Immediate action
Check build output for rendering mode
Commands
next build 2>&1 | grep -E '●|λ|○'
cat .next/server/pages/products/*.html 2>/dev/null | head -5
Fix now
Remove getServerSideProps and use getStaticProps — or check if dynamic API usage (cookies, headers) forces SSR
Build fails with getStaticPaths error+
Immediate action
Check that getStaticPaths returns correct shape
Commands
grep -rn 'getStaticPaths' pages/ --include='*.js' --include='*.tsx'
next build 2>&1 | grep -A5 'getStaticPaths'
Fix now
Ensure paths array items have { params: { key: value } } matching the dynamic route filename
Image optimization not working in production+
Immediate action
Check next.config.js for image domains
Commands
cat next.config.js | grep -A10 'images'
curl -sI http://localhost:3000/_next/image?url=/test.jpg | head -10
Fix now
Add remote image domains to next.config.js images.domains or images.remotePatterns
Hydration mismatch error in console+
Immediate action
Find components that render differently on server vs client
Commands
grep -rn 'typeof window\|Date.now\|Math.random' pages/ components/ --include='*.js' --include='*.tsx'
grep -rn 'useEffect\|useState' pages/ --include='*.js' --include='*.tsx'
Fix now
Move browser-dependent logic into useEffect or use dynamic import with ssr: false
Rendering Strategies and Router Comparison
FeaturePages Router (pages/)App Router (app/)
Introduced in versionNext.js 1 (original)Next.js 13
Default component typeClient ComponentServer Component
Data fetching methodgetStaticProps / getServerSidePropsasync/await directly in component
ISR supportrevalidate in getStaticProps{ next: { revalidate: N } } in fetch
Client interactivityAll components can use hooksRequires 'use client' directive
LayoutsCustom _app.js + _document.jsNested layout.js files per folder
Learning curveLower — familiar React patternsHigher — new mental model required
Production readinessBattle-tested, widely usedStable since Next.js 13.4
Best forExisting projects, teams new to Next.jsNew projects, performance-critical apps

Key takeaways

1
File-based routing means your folder structure in pages/ or app/ IS your navigation
no router config to maintain
2
SSG (getStaticProps) builds HTML at deploy time and serves from CDN
always default to this
3
ISR (adding revalidate
N to getStaticProps) gives you the speed of static with near-real-time freshness
4
getServerSideProps runs a server function on every request
use it only when data must be fresh per-request
5
In the App Router, components are Server Components by default
only add 'use client' when you need interactivity
6
Cache-Control headers and next/image optimisation are the biggest production performance levers

Common mistakes to avoid

5 patterns
×

Using getServerSideProps everywhere for 'freshness'

Symptom
Pages feel slow, Time to First Byte is high, server costs are unexpectedly large. Under traffic spikes, the server cannot generate enough HTML pages per second and starts dropping connections.
Fix
Default to getStaticProps with revalidate (ISR). Only reach for getServerSideProps when you genuinely need per-request data like session cookies or live stock prices. SSR runs a server function on every single page load — it is expensive and often unnecessary.
×

Putting a plain <a> tag inside next/link

Symptom
In older Next.js versions, you get a warning 'Do not use <a> inside <Link>'. In Next.js 13+, if you accidentally wrap a Link around an existing <a>, you get nested anchor tags which breaks accessibility and navigation.
Fix
In Next.js 13+, Link renders its own <a> tag automatically. Just put text or other non-anchor elements as children of Link. Only add an <a> tag as a child if you are on Next.js 12 or older.
×

Ignoring the fallback option in getStaticPaths

Symptom
You deploy, a user visits a new product page that was not in your build, and they hit a 404 even though the product exists in your database.
Fix
Set fallback: 'blocking' in getStaticPaths. This tells Next.js to SSR any page not pre-built, then cache it for future visitors. Your build only pre-generates popular pages (keeping build times fast) while still serving any valid URL correctly.
×

Not setting Cache-Control headers for static assets

Symptom
Every page load re-downloads JavaScript and CSS bundles from the origin server. Lighthouse reports high TTFB and recommends caching static assets. CDN cache hit rate is near zero.
Fix
Add Cache-Control headers in next.config.js: public, max-age=31536000, immutable for /_next/static/* assets. These files have content hashes in their filenames — they never change, so aggressive caching is safe.
×

Using next/image without width and height props

Symptom
Images cause Cumulative Layout Shift (CLS) — content jumps around as images load. Lighthouse CLS score is above 0.25 (poor). Google search ranking is penalised.
Fix
Always provide width and height to next/image. These props set the aspect ratio so the browser reserves space before the image loads. For responsive images, use fill={true} with a sized parent container instead.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between getStaticProps and getServerSideProps, an...
Q02SENIOR
What are React Server Components in Next.js's App Router, and why can't ...
Q03SENIOR
If a page uses getStaticProps with revalidate: 60, and two users hit tha...
Q04JUNIOR
What is the difference between the Pages Router and the App Router in Ne...
Q01 of 04SENIOR

What is the difference between getStaticProps and getServerSideProps, and how do you decide which one to use for a given page?

ANSWER
getStaticProps runs at build time — the HTML is generated once and served from a CDN. It is the fastest rendering strategy because no server work happens per request. Use it for content that does not change often: blog posts, product pages, marketing pages, documentation. getServerSideProps runs on every request — the server fetches data and builds HTML before sending it to the user. It is the most expensive strategy. Use it only when data must be fresh per-request AND needs to be in the initial HTML: dashboards showing live metrics, pages dependent on session cookies, personalised feeds. The decision framework: if the data can be stale for N seconds, use getStaticProps with revalidate: N (ISR). If the data must be fresh and the page must be SEO-indexed, use getServerSideProps. If the data is behind authentication and does not need SEO, use client-side fetching (useEffect or React Query) and skip server rendering entirely.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Do I need to know React before learning Next.js?
02
Is Next.js only for server-side rendering?
03
What is the difference between the app/ folder and the pages/ folder in Next.js 13+?
04
Can I deploy Next.js without Vercel?
05
How do I know which rendering strategy a page is using?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's React.js. Mark it forged?

12 min read · try the examples if you haven't

Previous
React Testing with Jest
13 / 47 · React.js
Next
React Custom Hooks