Kazi Rahamatullah
Kazi Rahamatullah
AboutProjectsBlogContact
UsesBooks
ResumeView CV
Kazi Rahamatullah

© Copyright 2026 Kazi Rahamatullah

AboutProjectsBlogBooksUses
Twitter/XGitHubProduct HuntCodeSandbox
Back to Blog
Next.jsCachingRenderingWeb PerformanceApp Router

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.

Jul 3, 202618 min read
Next.js Caching & Rendering

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:

  1. Rendering strategies — SSG, SSR, ISR, and CSR
  2. Core architecture — Server Components, Client Components, hydration, and the RSC payload
  3. Four caching layers — Request memoization, Data Cache, Full Route Cache, Router Cache
  4. Revalidation — time-based and on-demand refresh
  5. Advanced patterns — unstable_cache, use cache, and stale-while-revalidate
  6. CWV & SEO — Core Web Vitals, metadata, robots, and sitemaps
  7. Developer tools — next dev, next build, and cache inspection
  8. Proxy limits — what runs before caching (Next.js 16 proxy.ts)

Each section includes App Router examples for Next.js 15–16.

Note

Caching behavior depends on your rendering strategy and fetch options. A route marked force-dynamic skips the Full Route Cache; cache: 'no-store' bypasses the Data Cache. Always trace which layer applies before tuning revalidate.

Quick index

#Section
1Rendering strategies
2Core architecture
3Four caching layers
4Revalidation
5Advanced caching patterns
6CWV & SEO framework
7Developer tools
8Proxy & 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.

StrategyWhen it runsStrengthsTrade-offs
SSGBuild timeFastest delivery, low server loadStale until rebuild or revalidate
SSREvery requestReal-time, personalized dataHigher latency and server cost
ISRBuild + background refreshBalanced speed and freshnessComplexity in invalidation
CSRIn the browserRich interactions, browser APIsSlower first paint, SEO limits

Static Site Generation (SSG)

Pages rendered at build time and served as static HTML. Default for routes without dynamic APIs.

tsx
// 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'.

tsx
// 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.

tsx
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.

tsx
'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 / productPrimary strategyKey Next.js caches usedWhy this fits
Next.js docsSSGFull Route Cache, Data CacheDocs change at deploy — pre-render everything
Portfolio / agency siteSSGFull Route Cache, Router CacheSame HTML for every visitor; CDN-friendly
Blog / CMS (this site)ISRData Cache, Full Route Cache, Router CachePosts update without full rebuild; revalidateTag on publish
E-commerce catalog (Shopify on Next)ISRData Cache + on-demand revalidationProduct pages cached; webhook busts on stock/price change
News / media articlesISR (60s–5min)Data Cache (stale-while-revalidate)Fresh enough for readers; fast under traffic spikes
SaaS marketing pages (Stripe, Linear)SSGFull Route CacheLanding pages identical for all visitors
SaaS dashboard (Vercel, Linear app)SSRRequest memoization onlyPer-user data via cookies() — no static cache
Banking / healthcare portalSSRNone for user data (no-store)Personalized, regulated — must not cache PII
Social timeline widgetCSRRouter Cache (shell navigation)Real-time updates; browser fetch after hydration
Admin analytics panelCSR + SSR shellRouter CacheHeavy charts/tables client-side; auth shell on server
E-commerce checkoutSSRRequest memoizationCart/session-specific — dynamic every request
StrategyTypical sitesCache behavior summary
SSGDocs, portfolios, marketing landingsFull Route Cache hit → instant HTML at CDN
ISRBlogs, product pages, news, CMS-driven sitesData Cache + background revalidate on interval
SSRDashboards, account settings, checkout, authenticated appsSkips Full Route Cache; fresh render per request
CSRLive feeds, chat, rich editors, browser-only APIsClient fetch after load; Router Cache for nav only

Note

A single app commonly combines strategies: SSG for / and /about, ISR for /blog/[slug], SSR for /dashboard, and CSR widgets embedded inside Server Component layouts. Match the strategy to each route — not the whole project.

Tip

Default to static (SSG) for public pages. Opt into dynamic APIs only where personalization or real-time data requires SSR. Pair ISR with revalidateTag for CMS-driven sites.

Back to index


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.

tsx
// 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>
}
CapabilityServer ComponentClient Component
Database access✅❌
useState / useEffect❌✅
Event handlers❌✅
Bundle impactZero JS shippedJS shipped

Client Components

Render on the server for initial HTML, then hydrate on the client for interactivity.

tsx
'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.

Tip

Keep Client Component trees small — push interactivity to the leaves. A page that is 95% Server Components ships minimal JavaScript and benefits most from Full Route Cache and RSC payloads.

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.

tsx
import Link from 'next/link'
 
// Prefetches RSC payload when link enters viewport
;<Link href="/blog" prefetch>
  Read the blog
</Link>

Note

Server Components handle data fetching and layout. Client Components handle interactivity. Fetch in Server Components so Request Memoization and Data Cache apply — not in useEffect.

Back to index


3. Four caching layers

Next.js layers four distinct caches. They all avoid redoing work — but at different levels with different lifetimes.

plaintext
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

PropertyValue
OwnerReact (not Next.js-specific)
StorageServer in-memory
ScopeSingle render pass
PurposeDeduplicate identical fetch calls

During one server render, identical fetch URLs with the same options execute once.

tsx
// 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" />
    </>
  )
}

Note

Request memoization does not persist across requests or users. It lasts only for the duration of one render.

Layer 2 — Data Cache

PropertyValue
OwnerNext.js
StoragePersistent (.next/cache on disk)
ScopeAcross requests and deployments
Bypasscache: 'no-store' or revalidate: 0

Stores fetch results. Powers ISR and static pages under traffic spikes.

tsx
// 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

PropertyValue
StoresStatic HTML + RSC Payload
Applies toStatic routes only
BenefitFast First Contentful Paint (FCP)
PersistenceSurvives server restarts

Routes become dynamic (skip this cache) when they use:

  • export const dynamic = 'force-dynamic'
  • cookies() or headers()
  • fetch with cache: 'no-store'

Layer 4 — Router Cache

PropertyValue
StorageClient-side browser memory
PurposeInstant client navigations
PrefetchTriggered by <Link>
DurationSession-based (cleared on refresh)

Warning

The Router Cache is client-only. It does not replace server-side Data Cache or Full Route Cache. A hard refresh clears it; server caches survive.

Performance

Trace one route through all four layers before tuning revalidate. A slow page might be a Router Cache miss, a dynamic route skipping Full Route Cache, or duplicate fetches outside memoization scope.

Back to index


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

tsx
// 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.

tsx
// 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

tsx
'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')
}
MethodTriggerUse case
revalidate (time)IntervalBlogs, catalogs
revalidatePathManual / webhookPage-level invalidation
revalidateTagManual / webhookGranular data invalidation
Server ActionsUser mutationForms, CRUD operations

Tip

Tag fetches with next: { tags: ['posts'] } and invalidate by tag — finer control than revalidatePath('/') which busts entire sections.

Back to index


5. Advanced caching patterns

unstable_cache for non-fetch data

The Data Cache wraps fetch natively. Database and filesystem reads need unstable_cache:

tsx
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:

tsx
'use cache'
 
export async function getCachedCategories() {
  return db.category.findMany()
}

Warning

Check the Next.js docs for the latest use cache status — APIs may still be experimental across versions.

Stale-while-revalidate in practice

ISR and time-based revalidate both follow SWR:

tsx
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.

Back to index


6. CWV & SEO framework

Caching and rendering choices directly impact Core Web Vitals and search visibility.

Core Web Vitals (CWV)

MetricWhat it measuresRendering impact
LCPLargest Contentful PaintFull Route Cache + optimized images
INPInteraction to Next PaintSmall Client Component trees, less JS
CLSCumulative Layout ShiftReserved image dimensions, font loading
tsx
// 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"
    />
  )
}

Performance

SSG + Full Route Cache gives the fastest LCP for static content. For dynamic pages, cache data with ISR and stream Server Components with loading.tsx so something meaningful paints early.

Metadata & head elements

tsx
// 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

tsx
// 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',
  }
}
tsx
// 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

tsx
import Script from 'next/script'
 
export function Analytics() {
  return (
    <Script
      src="https://analytics.example.com/script.js"
      strategy="afterInteractive" // load after hydration — protects INP
    />
  )
}

Note

CSR-heavy pages score poorly on LCP and SEO because content is not in the initial HTML. Prefer Server Components for indexable content; reserve CSR for authenticated widgets.

Back to index


7. Developer tools

Understand what each CLI command does to the cache stack.

CommandWhat it does
next devDev server with hot reload — caches behave differently than production
next buildGenerates static pages, populates Full Route Cache and Data Cache
next startServes production build with full caching enabled

Inspecting cache behavior in development

bash
# Production build — see which routes are static vs dynamic
next build
 
# Output shows:
# ○ /about          (Static)
# λ /dashboard      (Dynamic / SSR)
# ● /blog/[slug]    (SSG with ISR)

Tip

Always verify caching in a production build (next build && next start). next dev disables or alters some caches — behavior you see locally may not match deployed performance.

Debugging duplicate fetches

If the same API fires multiple times per request:

  1. Check if calls use identical URL and options (memoization requires both)
  2. Confirm fetches happen in Server Components, not split across client boundaries
  3. Extract shared fetches into a single parent and pass data as props

Back to index


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.

LimitationDetail
No Data Cache accessProxy cannot read or write the Data Cache
No Full Route Cache accessCannot serve cached HTML from Proxy
fetch in Proxy not cached like Server ComponentsDifferent caching semantics
Runs on every matched requestKeep logic minimal — redirects and headers only
tsx
// 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*'],
}

Warning

Proxy cannot use the Data Cache or Full Route Cache. Auth redirects and header rewrites belong in Proxy; data fetching belongs in Server Components where all four cache layers apply. See Next.js Middleware & Proxy for the full Proxy guide.

Tip

Production checklist

  • Default public routes to static (SSG) with ISR where content updates periodically
  • Fetch in Server Components — not useEffect — for memoization and Data Cache
  • Tag data with next: { tags } for granular revalidateTag invalidation
  • Reserve cache: 'no-store' and force-dynamic for truly personalized routes
  • Keep Client Components small to protect INP and bundle size
  • Verify caching with next build && next start, not next dev alone
  • Generate metadata, sitemap.ts, and robots.ts for SEO
  • Use priority on LCP images; defer third-party scripts with next/script
  • Keep Proxy thin — no data fetching; see Proxy vs caching limits above

Interview Answer

"Explain the four Next.js cache layers."

Request memoization deduplicates identical fetches within one server render. Data Cache persists fetch results across requests (ISR). Full Route Cache stores pre-rendered HTML + RSC payload for static routes. Router Cache holds RSC payloads in the browser for fast client navigations. They stack: a Router Cache miss hits the server, which may serve from Full Route Cache, which may read from Data Cache, with memoization deduping within a single render pass.

Back to index


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.

Share this article

XLinkedInFacebook
Kazi Rahamatullah

Written by

Kazi Rahamatullah

FullStack Developer

X / TwitterGitHubLinkedIn

Subscribe to my newsletter

Stay up to date and get notified when I share new contents.

No spam ever, unsubscribe anytime