Caching Strategies for Next.js: From Basics to Edge
Back to Blog

Caching Strategies for Next.js: From Basics to Edge

March 21, 20263 min read51 views

Caching is the most powerful optimization available—and the most misunderstood. Next.js 15 changed caching defaults significantly, moving from 'cache everything' to 'cache nothing by default.' Understanding the caching model is now essential.

The Next.js Caching Layers Explained

Next.js has four distinct caching mechanisms:

Request Memoization: Deduplicates identical requests within a single render pass.

Data Cache: Stores fetch results across requests and deployments.

Full Route Cache: Stores complete rendered HTML for static routes at build time.

Router Cache: Client-side cache for prefetched and visited pages.

Request Memoization and Data Cache

// Request memoization - automatic for same URL/options in a render
async function ComponentA() {
  const data = await fetch('/api/data') // Request 1
}
async function ComponentB() {
  const data = await fetch('/api/data') // Uses Request 1's result
}

// Data Cache - controlled through fetch options
fetch('/api/data') // No caching (default in Next.js 15)
fetch('/api/data', { cache: 'force-cache' }) // Cache forever
fetch('/api/data', { next: { revalidate: 60 } }) // Cache 60 seconds
fetch('/api/data', { next: { tags: ['posts'] } }) // Tag-based invalidation

Full Route Cache and Static Generation

// Static page - cached at build time
export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)
  return <article>{post.content}</article>
}

export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

// Route segment config
export const dynamic = 'force-static'  // Error if dynamic features used
export const dynamic = 'force-dynamic' // Never cache
export const revalidate = 3600         // Seconds

CDN Caching and Headers

export async function GET() {
  const data = await fetchExpensiveData()
  
  return NextResponse.json(data, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=3600',
    },
  })
}

Debugging Cache Behavior

Common gotchas:

// GOTCHA: Dynamic functions make the whole route dynamic
export default async function Page() {
  const headersList = headers() // This makes the page dynamic!
  const data = await fetch('/api/data', { cache: 'force-cache' })
  // Even though fetch is cached, page renders dynamically
}

// SOLUTION: Move dynamic parts to client components
export default function Page() {
  return (
    <>
      <StaticContent />
      <ClientAuthSection /> {/* Dynamic on client */}
    </>
  )
}

Revalidation Strategies

// On-demand revalidation by tag
import { revalidateTag } from 'next/cache'

async function getPost(slug) {
  return fetch(`/api/posts/${slug}`, {
    next: { tags: ['posts', `post-${slug}`] }
  })
}

// When a post is updated:
revalidateTag(`post-${slug}`) // Just this post
revalidateTag('posts')        // All posts

Key Takeaways

Request Memoization is free and automatic—structure components to benefit.

Data Cache requires explicit configuration in Next.js 15.

Full Route Cache is the biggest performance win—design routes to be static.

Debug systematically. Check rendering mode, inspect headers, understand what makes routes dynamic.

Share this article