Next.js

Caching Strategies in Next.js

35 min Lesson 21 of 40

Understanding Caching in Next.js

Caching is one of the most powerful features in Next.js 13+ with the App Router. Next.js provides multiple layers of caching to optimize performance and reduce server load. Understanding these caching mechanisms is crucial for building fast, efficient applications.

Types of Caching in Next.js

Next.js implements four main caching mechanisms:

  • Request Memoization: Deduplicates requests within a single render pass
  • Data Cache: Persists data fetching results across requests
  • Full Route Cache: Caches rendered HTML and RSC payload at build time
  • Router Cache: Client-side cache for visited routes
Important: Caching behavior differs between development and production. Most aggressive caching only occurs in production builds.

Request Memoization

Request memoization automatically deduplicates fetch requests with the same URL and options within a single render pass:

// app/products/[id]/page.tsx
async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`);
  return res.json();
}

async function getProductReviews(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`);
  return res.json();
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  // These two calls are deduplicated - only one request is made
  const product = await getProduct(params.id);
  const reviews = await getProductReviews(params.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <ReviewsList reviews={reviews} />
    </div>
  );
}
Tip: Request memoization only works within the same render tree. Requests in different components during the same render are deduplicated automatically.

Data Cache (Fetch Cache)

The Data Cache persists fetch results across requests and deployments. By default, all fetch requests are cached indefinitely:

// Default: cached indefinitely
async function getStaticData() {
  const res = await fetch('https://api.example.com/static-data');
  return res.json();
}

// Opt out of caching
async function getDynamicData() {
  const res = await fetch('https://api.example.com/dynamic-data', {
    cache: 'no-store'
  });
  return res.json();
}

// Revalidate after 60 seconds
async function getRevalidatedData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 }
  });
  return res.json();
}

Cache Options

Next.js provides several options for controlling data caching:

// Force cache (default behavior)
fetch(url, { cache: 'force-cache' });

// Never cache
fetch(url, { cache: 'no-store' });

// Revalidate after X seconds
fetch(url, { next: { revalidate: 3600 } });

// Revalidate based on tags
fetch(url, { next: { tags: ['products'] } });

Full Route Cache

The Full Route Cache stores the rendered HTML and React Server Component payload at build time for static routes:

// app/blog/[slug]/page.tsx

// Generate static params at build time
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return posts.map((post: any) => ({
    slug: post.slug,
  }));
}

// This page will be statically generated at build time
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}
Warning: Routes that use dynamic functions (cookies(), headers(), searchParams) cannot be fully cached and will be rendered dynamically.

Router Cache (Client-Side Cache)

The Router Cache is an in-memory client-side cache that stores the RSC payload of visited routes:

// The Router Cache automatically stores visited routes
// You can control its behavior with router options

// app/layout.tsx
import { Link } from 'next/link';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {/* prefetch: controls prefetching behavior */}
        <Link href="/about" prefetch={true}>About</Link>
        <Link href="/contact" prefetch={false}>Contact</Link>
        {children}
      </body>
    </html>
  );
}

Cache Tags and Revalidation

Cache tags allow you to invalidate specific cache entries on-demand:

// app/products/page.tsx
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] }
  });
  return res.json();
}

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const tag = request.nextUrl.searchParams.get('tag');

  if (tag) {
    revalidateTag(tag);
    return Response.json({ revalidated: true, now: Date.now() });
  }

  return Response.json({ revalidated: false }, { status: 400 });
}

Path-Based Revalidation

You can also revalidate specific paths:

// app/api/revalidate-path/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const path = request.nextUrl.searchParams.get('path');

  if (path) {
    revalidatePath(path);
    return Response.json({ revalidated: true, path });
  }

  return Response.json({ revalidated: false }, { status: 400 });
}

// Usage: POST to /api/revalidate-path?path=/products

Time-Based Revalidation

Configure automatic revalidation at the route segment level:

// app/products/page.tsx

// Revalidate this page every 60 seconds
export const revalidate = 60;

export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products').then(r => r.json());

  return (
    <div>
      <h1>Products</h1>
      <ProductList products={products} />
    </div>
  );
}

Opt Out of Caching

Several ways to opt out of caching for dynamic content:

// Method 1: Use dynamic functions
import { cookies } from 'next/headers';

export default async function DynamicPage() {
  const cookieStore = cookies();
  // This makes the page dynamic
  return <div>Dynamic content</div>;
}

// Method 2: Set dynamic route segment config
export const dynamic = 'force-dynamic';

// Method 3: Use cache: 'no-store' in fetch
async function getData() {
  const res = await fetch(url, { cache: 'no-store' });
  return res.json();
}

// Method 4: Disable caching for entire route
export const fetchCache = 'force-no-store';
Route Segment Config Options:
  • dynamic: 'auto' | 'force-dynamic' | 'error' | 'force-static'
  • revalidate: false | number (seconds)
  • fetchCache: 'auto' | 'force-cache' | 'only-cache' | 'force-no-store'
  • runtime: 'nodejs' | 'edge'

Caching Best Practices

// 1. Cache static content aggressively
async function getStaticContent() {
  return fetch('https://api.example.com/static', {
    next: { revalidate: false } // Cache indefinitely
  });
}

// 2. Use appropriate revalidation times
async function getBlogPosts() {
  return fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // Revalidate every hour
  });
}

// 3. Use tags for related content
async function getProductData(id: string) {
  const [product, reviews, related] = await Promise.all([
    fetch(`https://api.example.com/products/${id}`, {
      next: { tags: ['products', `product-${id}`] }
    }),
    fetch(`https://api.example.com/products/${id}/reviews`, {
      next: { tags: [`product-${id}-reviews`] }
    }),
    fetch(`https://api.example.com/products/${id}/related`, {
      next: { tags: ['products', 'related-products'] }
    })
  ]);

  return { product, reviews, related };
}

// 4. Don't cache user-specific data
async function getUserData(userId: string) {
  return fetch(`https://api.example.com/users/${userId}`, {
    cache: 'no-store' // Always fresh
  });
}

Debugging Cache Behavior

Next.js provides headers to help debug caching:

// app/api/debug/route.ts
export async function GET() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 60, tags: ['debug'] }
  });

  return Response.json({
    data: await data.json(),
    headers: {
      'x-vercel-cache': data.headers.get('x-vercel-cache'),
      'cache-control': data.headers.get('cache-control'),
    }
  });
}
Cache Status Headers:
  • HIT: Data served from cache
  • MISS: Data fetched from origin, added to cache
  • STALE: Stale data served, revalidating in background
  • BYPASS: Cache bypassed (cache: 'no-store')

Practice Exercise

Task: Build a news website with proper caching:

  1. Create a homepage that caches for 5 minutes
  2. Individual articles should be statically generated at build time
  3. Implement an API route to revalidate articles by tag
  4. User comments should never be cached
  5. Add cache status debugging to your API routes

Bonus: Implement a "Latest News" section that updates every minute while the main content updates every 5 minutes.

Summary

Key takeaways about caching in Next.js:

  • Next.js provides four caching layers: Request Memoization, Data Cache, Full Route Cache, and Router Cache
  • Use time-based revalidation for content that updates regularly
  • Use tag-based revalidation for on-demand cache invalidation
  • Static content should be cached aggressively, user-specific data should not be cached
  • Use route segment config options to control caching behavior at the route level
  • Monitor cache status headers to debug caching issues