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
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.
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';
exportdefaultfunctionProductPage() {
// useRouter gives us access to the URL parametersconst 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 stateif (!productId) {
return <p>Loading product...</p>;
}
return (
<div>
<h1>ProductDetails</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 minuteexportdefaultfunctionBlogPost({ 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 requestexportasyncfunctiongetStaticPaths() {
// In real life, fetch this list from your CMS or databaseconst 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)exportasyncfunctiongetStaticProps({ 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.
The Next.js Link and Image Components: Not Just Wrappers
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/imageimportLinkfrom'next/link';
importImagefrom'next/image';
exportdefaultfunctionProductCard({ 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)
- PreventsCLS (CumulativeLayoutShift) 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+JSin the background when visible
- Falls back to normal <a> behaviour ifJavaScript hasn't loaded yet
*/}
<Link href={`/products/${product.id}`}>
{/* AsofNext.js 13+, you don't need a nested <a> tag anymore */}
ViewProductDetails
</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.exportdefaultasyncfunctionProductPage({ params }) {
// params comes from the folder name [productId] — same concept as Pages Routerconst { productId } = params;
// fetch() in Server Components is enhanced by Next.js// next: { revalidate: 3600 } is equivalent to ISR — cache for 1 hourconst response = awaitfetch(
`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.
TheServerComponent passes data down; the ClientComponent 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';
exportfunctionAddToCartButton({ productId }) {
const [added, setAdded] = useState(false);
functionhandleClick() {
// 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 exportimport { NextResponse } from'next/server';
// GET /api/users — list all usersexportasyncfunctionGET(request) {
const { searchParams } = newURL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '20');
// In production: call your database hereconst users = await db.users.findMany({
skip: (page - 1) * limit,
take: limit,
});
returnNextResponse.json({
data: users,
pagination: { page, limit },
});
}
// POST /api/users — create a new userexportasyncfunctionPOST(request) {
const body = await request.json();
// Validate inputif (!body.email || !body.name) {
returnNextResponse.json(
{ error: 'Email and name are required' },
{ status: 400 }
);
}
const user = await db.users.create({ data: body });
returnNextResponse.json({ data: user }, { status: 201 });
}
// --- Middleware ---// File: middleware.js (project root)// Runs on every request before pages/API routesimport { NextResponse } from'next/server';
exportfunctionmiddleware(request) {
// Example: redirect unauthenticated usersconst token = request.cookies.get('auth-token');
// Protect dashboard routesif (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
returnNextResponse.redirect(newURL('/login', request.url));
}
// Add request ID for tracingconst response = NextResponse.next();
response.headers.set('x-request-id', crypto.randomUUID());
return response;
}
// Matcher controls which routes trigger middlewareexportconst 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 assetsasyncheaders() {
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 SEOasyncredirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true, // 301 redirect — SEO-friendly
},
];
},
// Rewrites: proxy external APIs through Next.jsasyncrewrites() {
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(
newBundleAnalyzerPlugin({
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.
● 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
Move browser-dependent logic into useEffect or use dynamic import with ssr: false
Rendering Strategies and Router Comparison
Feature
Pages Router (pages/)
App Router (app/)
Introduced in version
Next.js 1 (original)
Next.js 13
Default component type
Client Component
Server Component
Data fetching method
getStaticProps / getServerSideProps
async/await directly in component
ISR support
revalidate in getStaticProps
{ next: { revalidate: N } } in fetch
Client interactivity
All components can use hooks
Requires 'use client' directive
Layouts
Custom _app.js + _document.js
Nested layout.js files per folder
Learning curve
Lower — familiar React patterns
Higher — new mental model required
Production readiness
Battle-tested, widely used
Stable since Next.js 13.4
Best for
Existing projects, teams new to Next.js
New 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.
Q02 of 04SENIOR
What are React Server Components in Next.js's App Router, and why can't you use useState or useEffect inside them?
ANSWER
React Server Components (RSCs) are components that execute only on the server. They render to an RSC payload — a special format that the client can reconstruct into React elements — with zero JavaScript shipped to the browser for those components.
You cannot use useState or useEffect in Server Components because these hooks depend on the React client runtime. useState manages client-side state that persists across re-renders in the browser. useEffect runs side effects after the component mounts in the browser. Server Components do not mount in the browser — they render once on the server and send their output as HTML or RSC payload. There is no client runtime to execute these hooks.
If a component needs interactivity (state, effects, event handlers), add 'use client' at the top of the file. This tells Next.js to include the component in the client JavaScript bundle. The key principle: keep as many components as Server Components as possible — only make components client-side when they specifically need browser APIs or React hooks.
Q03 of 04SENIOR
If a page uses getStaticProps with revalidate: 60, and two users hit that page within the same minute — what does each user actually receive? Walk me through what Next.js does behind the scenes.
ANSWER
This is ISR (Incremental Static Regeneration) in action.
First user (after the cache expires): Next.js serves the stale (cached) HTML immediately — the user does not wait. In the background, Next.js re-runs getStaticProps, generates fresh HTML, and replaces the cached version. The first user sees the old data.
Second user (within the 60-second window): Next.js serves the freshly generated HTML from the background revalidation. This user sees the new data.
If no user visits for 60+ seconds: the cache is stale but not yet regenerated. The next user triggers the background regeneration and sees stale data. The user after that sees fresh data.
The key insight: ISR never makes a user wait for revalidation. The stale-while-revalidate pattern means every user gets an instant response — either from the existing cache or from a background-generated replacement. No user is ever blocked waiting for getStaticProps to complete.
This is why ISR is superior to SSR for most use cases: the user experience is identical to static (instant), but the data freshness approaches real-time (within the revalidation window).
Q04 of 04JUNIOR
What is the difference between the Pages Router and the App Router in Next.js?
ANSWER
The Pages Router is the original routing system — files in the pages/ folder become routes. Components are Client Components by default. Data fetching uses getStaticProps (SSG) and getServerSideProps (SSR). It has been battle-tested since Next.js 1.
The App Router is the newer system introduced in Next.js 13 — files in the app/ folder become routes. Components are Server Components by default (zero JS shipped). Data fetching uses async/await directly in components with fetch() caching options. It supports nested layouts, streaming, and React Server Components.
The key differences: App Router ships less JavaScript (Server Components), has a simpler data fetching model (just use fetch), and supports nested layouts per route segment. Pages Router is simpler to learn and is used in most existing codebases. New projects should use the App Router. Existing projects can migrate gradually — both routers coexist in the same project.
01
What is the difference between getStaticProps and getServerSideProps, and how do you decide which one to use for a given page?
SENIOR
02
What are React Server Components in Next.js's App Router, and why can't you use useState or useEffect inside them?
SENIOR
03
If a page uses getStaticProps with revalidate: 60, and two users hit that page within the same minute — what does each user actually receive? Walk me through what Next.js does behind the scenes.
SENIOR
04
What is the difference between the Pages Router and the App Router in Next.js?
JUNIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
Do I need to know React before learning Next.js?
Yes — Next.js is a framework built on top of React, not a replacement for it. You should be comfortable with React components, props, state, and hooks before starting with Next.js. The framework handles routing, rendering strategy, and optimisation on top of the React you already know.
Was this helpful?
02
Is Next.js only for server-side rendering?
No — this is one of the biggest misconceptions. Next.js supports static generation (SSG), server-side rendering (SSR), client-side rendering (CSR), and incremental static regeneration (ISR) all in the same project, on a page-by-page basis. Most production apps use a mix of all four strategies depending on what each page needs.
Was this helpful?
03
What is the difference between the app/ folder and the pages/ folder in Next.js 13+?
They represent two different routing and rendering systems that can coexist in one project. The pages/ folder uses the original Pages Router with getStaticProps and getServerSideProps. The app/ folder uses the newer App Router where components are Server Components by default and data fetching happens with async/await directly in components. New projects should use the App Router; existing projects can migrate gradually.
Was this helpful?
04
Can I deploy Next.js without Vercel?
Yes. Next.js deploys anywhere: AWS (with SST or Amplify), Google Cloud, Docker containers, Netlify, or static hosting for fully-static sites. Vercel provides the tightest integration (automatic ISR caching, edge functions, preview deployments) but is not required. For self-hosted deployments, use next start with a Node.js server or export a static site with next export for fully-static applications.
Was this helpful?
05
How do I know which rendering strategy a page is using?
Run next build — the output shows each page with a symbol: ● (static/SSG), λ (server/SSR), or ○ (static with ISR). In development mode, check the terminal output or use the Next.js DevTools browser extension. You can also check the response headers — static pages have different cache headers than server-rendered pages.