Next.js Caching & Rendering
Deep dive into Next.js rendering strategies, Server and Client Components, the four caching layers, revalidation, Core Web Vitals, SEO, and App Router patterns for 2026.

Introduction
If a page feels instant on the second visit but stale after a deploy — or the same fetch runs three times in one render — you are bumping into rendering and caching working together.
Next.js does not just render React on the server. It orchestrates when HTML is produced, what gets stored, and how long that output stays valid. Four separate cache layers cooperate with your rendering strategy — confuse them and debugging becomes painful.
This guide covers:
- Rendering strategies — SSG, SSR, ISR, and CSR
- Core architecture — Server Components, Client Components, hydration, and the RSC payload
- Four caching layers — Request memoization, Data Cache, Full Route Cache, Router Cache
- Revalidation — time-based and on-demand refresh
- Advanced patterns —
unstable_cache,use cache, and stale-while-revalidate - CWV & SEO — Core Web Vitals, metadata, robots, and sitemaps
- Developer tools —
next dev,next build, and cache inspection - Proxy limits — what runs before caching (Next.js 16
proxy.ts)
Each section includes App Router examples for Next.js 15–16.
Quick index
| # | Section |
|---|---|
| 1 | Rendering strategies |
| 2 | Core architecture |
| 3 | Four caching layers |
| 4 | Revalidation |
| 5 | Advanced caching patterns |
| 6 | CWV & SEO framework |
| 7 | Developer tools |
| 8 | Proxy & caching limits |
1. Rendering strategies
In the App Router, the rendering strategy is often implicit — determined by component type, fetch options, and route segment config.
| Strategy | When it runs | Strengths | Trade-offs |
|---|---|---|---|
| SSG | Build time | Fastest delivery, low server load | Stale until rebuild or revalidate |
| SSR | Every request | Real-time, personalized data | Higher latency and server cost |
| ISR | Build + background refresh | Balanced speed and freshness | Complexity in invalidation |
| CSR | In the browser | Rich interactions, browser APIs | Slower first paint, SEO limits |
Static Site Generation (SSG)
Pages rendered at build time and served as static HTML. Default for routes without dynamic APIs.
// app/projects/page.tsx — static when data is local or cached fetch
import { getAllProjects } from '@/lib/projects'
export default async function ProjectsPage() {
const { data: projects } = getAllProjects()
return <ProjectGrid projects={projects} />
}Best for: marketing pages, documentation, portfolios.
Server-Side Rendering (SSR)
Rendered on every request. Triggered by cookies(), headers(), searchParams, or dynamic = 'force-dynamic'.
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
export const dynamic = 'force-dynamic'
export default async function DashboardPage() {
const session = (await cookies()).get('session')?.value
const data = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${session}` },
cache: 'no-store',
})
return <Dashboard data={await data.json()} />
}Best for: personalized dashboards, authenticated views.
Incremental Static Regeneration (ISR)
Static pages rebuilt in the background on a schedule or on-demand — no full site rebuild.
export const revalidate = 60
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
})
return <PostList posts={await posts.json()} />
}Best for: blogs, product catalogs, CMS-driven content.
Client-Side Rendering (CSR)
Data fetched in the browser after the shell loads.
'use client'
import { useEffect, useState } from 'react'
export function LiveActivityFeed() {
const [events, setEvents] = useState([])
useEffect(() => {
fetch('/api/activity')
.then((res) => res.json())
.then(setEvents)
}, [])
return (
<ul>
{events.map((e) => (
<li key={e.id}>{e.label}</li>
))}
</ul>
)
}Best for: live feeds, widgets needing browser-only APIs.
Real-world strategy comparison
Production apps rarely pick one strategy for the entire site. They mix per route — static marketing, ISR for content, SSR for dashboards, CSR for interactive widgets.
| Website / product | Primary strategy | Key Next.js caches used | Why this fits |
|---|---|---|---|
| Next.js docs | SSG | Full Route Cache, Data Cache | Docs change at deploy — pre-render everything |
| Portfolio / agency site | SSG | Full Route Cache, Router Cache | Same HTML for every visitor; CDN-friendly |
| Blog / CMS (this site) | ISR | Data Cache, Full Route Cache, Router Cache | Posts update without full rebuild; revalidateTag on publish |
| E-commerce catalog (Shopify on Next) | ISR | Data Cache + on-demand revalidation | Product pages cached; webhook busts on stock/price change |
| News / media articles | ISR (60s–5min) | Data Cache (stale-while-revalidate) | Fresh enough for readers; fast under traffic spikes |
| SaaS marketing pages (Stripe, Linear) | SSG | Full Route Cache | Landing pages identical for all visitors |
| SaaS dashboard (Vercel, Linear app) | SSR | Request memoization only | Per-user data via cookies() — no static cache |
| Banking / healthcare portal | SSR | None for user data (no-store) | Personalized, regulated — must not cache PII |
| Social timeline widget | CSR | Router Cache (shell navigation) | Real-time updates; browser fetch after hydration |
| Admin analytics panel | CSR + SSR shell | Router Cache | Heavy charts/tables client-side; auth shell on server |
| E-commerce checkout | SSR | Request memoization | Cart/session-specific — dynamic every request |
| Strategy | Typical sites | Cache behavior summary |
|---|---|---|
| SSG | Docs, portfolios, marketing landings | Full Route Cache hit → instant HTML at CDN |
| ISR | Blogs, product pages, news, CMS-driven sites | Data Cache + background revalidate on interval |
| SSR | Dashboards, account settings, checkout, authenticated apps | Skips Full Route Cache; fresh render per request |
| CSR | Live feeds, chat, rich editors, browser-only APIs | Client fetch after load; Router Cache for nav only |
2. Core architecture
Before the cache layers make sense, you need the component model and what travels over the wire.
Server Components
Render only on the server. Direct database access, filesystem reads, and secrets — without shipping that logic to the browser.
// Server Component — no 'use client'
import { getPostBySlug } from '@/lib/blog'
export default async function BlogPost({ slug }: { slug: string }) {
const post = getPostBySlug(slug)
return <article>{post?.title}</article>
}| Capability | Server Component | Client Component |
|---|---|---|
| Database access | ✅ | ❌ |
useState / useEffect | ❌ | ✅ |
| Event handlers | ❌ | ✅ |
| Bundle impact | Zero JS shipped | JS shipped |
Client Components
Render on the server for initial HTML, then hydrate on the client for interactivity.
'use client'
import { useState } from 'react'
export function ThemeToggle() {
const [dark, setDark] = useState(false)
return <button onClick={() => setDark(!dark)}>Toggle theme</button>
}Hydration
Hydration attaches event listeners and client state to server-rendered HTML. Until it completes, interactive elements may not respond.
RSC payload
App Router navigations often send an RSC Payload — a serialized component tree — instead of full HTML. The client merges it into the DOM efficiently.
This is why <Link> navigations feel fast: the Router Cache stores these payloads for instant back/forward navigation.
import Link from 'next/link'
// Prefetches RSC payload when link enters viewport
;<Link href="/blog" prefetch>
Read the blog
</Link>3. Four caching layers
Next.js layers four distinct caches. They all avoid redoing work — but at different levels with different lifetimes.
User Request
│
▼
┌─────────────────┐
│ Router Cache │ ← Client: instant back/forward nav
└────────┬────────┘
│ (cache miss)
▼
┌─────────────────┐
│ Full Route Cache│ ← Server: static HTML + RSC payload
└────────┬────────┘
│ (dynamic route or stale)
▼
┌─────────────────┐
│ Request Memo │ ← Dedupe fetches in one render
└────────┬────────┘
│
▼
┌─────────────────┐
│ Data Cache │ ← Persisted fetch / unstable_cache results
└─────────────────┘Layer 1 — Request memoization
| Property | Value |
|---|---|
| Owner | React (not Next.js-specific) |
| Storage | Server in-memory |
| Scope | Single render pass |
| Purpose | Deduplicate identical fetch calls |
During one server render, identical fetch URLs with the same options execute once.
// lib/user.ts
export async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`)
return res.json()
}
// app/profile/page.tsx
export default async function ProfilePage() {
// UserAvatar and UserStats both call getUser('42') — ONE network request
return (
<>
<UserAvatar userId="42" />
<UserStats userId="42" />
</>
)
}Layer 2 — Data Cache
| Property | Value |
|---|---|
| Owner | Next.js |
| Storage | Persistent (.next/cache on disk) |
| Scope | Across requests and deployments |
| Bypass | cache: 'no-store' or revalidate: 0 |
Stores fetch results. Powers ISR and static pages under traffic spikes.
// Cached — revalidate every hour
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600, tags: ['products'] },
})
// Never cached — always hits origin
const stock = await fetch('https://api.example.com/stock', {
cache: 'no-store',
})Layer 3 — Full Route Cache
| Property | Value |
|---|---|
| Stores | Static HTML + RSC Payload |
| Applies to | Static routes only |
| Benefit | Fast First Contentful Paint (FCP) |
| Persistence | Survives server restarts |
Routes become dynamic (skip this cache) when they use:
export const dynamic = 'force-dynamic'cookies()orheaders()fetchwithcache: 'no-store'
Layer 4 — Router Cache
| Property | Value |
|---|---|
| Storage | Client-side browser memory |
| Purpose | Instant client navigations |
| Prefetch | Triggered by <Link> |
| Duration | Session-based (cleared on refresh) |
4. Revalidation
Cached data eventually needs to refresh. Next.js offers time-based and on-demand models — both follow stale-while-revalidate: serve cached content immediately, refresh in the background.
Time-based revalidation
// Route segment config
export const revalidate = 300 // 5 minutes
// Per-fetch config
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 300 },
})After the interval expires, the next request triggers background revalidation. Users still receive the stale version instantly.
On-demand revalidation
Invalidate cache entries when content changes — ideal for CMS webhooks or after mutations.
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ message: 'Invalid secret' }, { status: 401 })
}
revalidateTag('products')
revalidatePath('/shop')
return NextResponse.json({ revalidated: true, now: Date.now() })
}Server Actions
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
await db.post.create({ data: { title: formData.get('title') as string } })
revalidatePath('/blog')
}| Method | Trigger | Use case |
|---|---|---|
revalidate (time) | Interval | Blogs, catalogs |
revalidatePath | Manual / webhook | Page-level invalidation |
revalidateTag | Manual / webhook | Granular data invalidation |
| Server Actions | User mutation | Forms, CRUD operations |
5. Advanced caching patterns
unstable_cache for non-fetch data
The Data Cache wraps fetch natively. Database and filesystem reads need unstable_cache:
import { unstable_cache } from 'next/cache'
import { db } from '@/lib/db'
export const getFeaturedPosts = unstable_cache(
async () => {
return db.post.findMany({ where: { featured: true }, take: 5 })
},
['featured-posts'],
{ revalidate: 600, tags: ['posts'] },
)use cache directive
Next.js is introducing a declarative use cache directive for function and component-level caching:
'use cache'
export async function getCachedCategories() {
return db.category.findMany()
}Stale-while-revalidate in practice
ISR and time-based revalidate both follow SWR:
export const revalidate = 60
export default async function Page() {
// Cached version served instantly.
// After 60s, next visitor triggers background refresh.
const data = await fetch('https://api.example.com/feed', {
next: { revalidate: 60 },
})
return <Feed items={await data.json()} />
}Pairing with TanStack Query
Server caches (Data Cache, Full Route Cache) and client caches (TanStack Query) solve different problems. Use Next.js caching for SSR/SSG pages; use TanStack Query for client-side mutations, optimistic updates, and shared server state after hydration.
6. CWV & SEO framework
Caching and rendering choices directly impact Core Web Vitals and search visibility.
Core Web Vitals (CWV)
| Metric | What it measures | Rendering impact |
|---|---|---|
| LCP | Largest Contentful Paint | Full Route Cache + optimized images |
| INP | Interaction to Next Paint | Small Client Component trees, less JS |
| CLS | Cumulative Layout Shift | Reserved image dimensions, font loading |
// Improve LCP — prioritize hero image
import Image from 'next/image'
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Portfolio hero"
width={1200}
height={630}
priority // preload for LCP element
className="h-auto w-full"
/>
)
}Metadata & head elements
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
images: [{ url: post.coverImage }],
},
}
}Robots & sitemap
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/', disallow: '/dashboard/' },
sitemap: 'https://example.com/sitemap.xml',
}
}// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { getAllPosts } from '@/lib/blog'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = getAllPosts()
return [
{ url: 'https://example.com', lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
...posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'monthly' as const,
priority: 0.8,
})),
]
}next/script optimization
import Script from 'next/script'
export function Analytics() {
return (
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive" // load after hydration — protects INP
/>
)
}7. Developer tools
Understand what each CLI command does to the cache stack.
| Command | What it does |
|---|---|
next dev | Dev server with hot reload — caches behave differently than production |
next build | Generates static pages, populates Full Route Cache and Data Cache |
next start | Serves production build with full caching enabled |
Inspecting cache behavior in development
# Production build — see which routes are static vs dynamic
next build
# Output shows:
# ○ /about (Static)
# λ /dashboard (Dynamic / SSR)
# ● /blog/[slug] (SSG with ISR)Debugging duplicate fetches
If the same API fires multiple times per request:
- Check if calls use identical URL and options (memoization requires both)
- Confirm fetches happen in Server Components, not split across client boundaries
- Extract shared fetches into a single parent and pass data as props
8. Proxy & caching limits
In Next.js 16, request interception runs in proxy.ts (formerly middleware.ts). Proxy executes before your route — and outside the full caching stack.
| Limitation | Detail |
|---|---|
| No Data Cache access | Proxy cannot read or write the Data Cache |
| No Full Route Cache access | Cannot serve cached HTML from Proxy |
fetch in Proxy not cached like Server Components | Different caching semantics |
| Runs on every matched request | Keep logic minimal — redirects and headers only |
// proxy.ts — Next.js 16+ (was middleware.ts)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const token = request.cookies.get('token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*'],
}Summary
Next.js caching is a stack of cooperating layers, not one switch. Rendering strategies decide when HTML is produced; four caches decide what gets reused. Default to static, add revalidation when content changes, keep Client Components small, and fetch on the server.
Related reads: Next.js Middleware & Proxy for request interception before caching, and TanStack Query Deep Dive for client-side server state after hydration.
Subscribe to my newsletter
Stay up to date and get notified when I share new contents.
No spam ever, unsubscribe anytime