DEV Community

ANKIT MAURYA
ANKIT MAURYA

Posted on • Originally published at Medium on

The Complete Guide to Next.js Caching: From Zero to Hero

Next.js 14 vs 15 vs 16: Complete Caching Tutorial for Beginners to Hero (2026)

Learn Next.js caching from scratch! Covers versions 14, 15, and 16 with real examples, migration guides, and best practices. Perfect for beginners. Updated Feb 2026.

Imagine you’re running a restaurant. Every time a customer orders your famous lasagna, you could:

  1. Option A : Start from scratch — buy ingredients, prepare the sauce, cook the pasta, layer everything, and bake it (takes 2 hours)
  2. Option B : Make a big batch in the morning, keep portions ready, and just heat them up when ordered (takes 5 minutes)

Caching is Option B  — but for your website.

When you cache data, you’re storing the result of expensive operations (like database queries or API calls) so you can reuse them instead of doing the work again. This makes your website:

  • Faster  — Users see content instantly
  • 💰 Cheaper  — Fewer server requests mean lower hosting costs
  • 😊 Better for users  — Fast websites make users happy

But here’s the catch: caching in Next.js has changed dramatically across versions. What worked in Next.js 14 might break your app in Next.js 15. This guide will help you understand everything from the ground up.

Part 1: Understanding Caching Fundamentals

Before we dive into Next.js specifics, let’s understand what caching actually means.


Part 1 : Understanding Caching Fundamentals (The Foundation)

What is Caching? The Complete Picture

Caching is the practice of storing data in a temporary storage location (the “cache”) so future requests for that data can be served faster.

Think of it like this:

WITHOUT CACHE:
User visits your blog → Server fetches from database (500ms) → Server generates HTML (200ms) → User sees page (700ms total)
User visits again → Server fetches from database (500ms) → Server generates HTML (200ms) → User sees page (700ms total)
Every. Single. Time.

WITH CACHE:
User visits your blog → Server fetches from database (500ms) → Server generates HTML (200ms) → SAVE TO CACHE → User sees page (700ms total)
User visits again → Server reads from cache (10ms) → User sees page (10ms total) ⚡
Enter fullscreen mode Exit fullscreen mode

Types of Caching in Web Applications

1. Browser Cache (Client-Side)

Your browser saves files (images, CSS, JavaScript) on your computer. When you revisit a website, it loads faster because it doesn’t re-download everything.

First visit: Downloads 2MB of files (slow 🐌)
Second visit: Uses cached files (fast 🚀)
Enter fullscreen mode Exit fullscreen mode

2. CDN Cache (Edge Network)

Content Delivery Networks (like Cloudflare, Vercel Edge) cache your content on servers around the world, closer to your users.

User in India → Fetches from India CDN server (50ms)
User in USA → Fetches from USA CDN server (50ms)
Both get fast responses, instead of everyone hitting your main server in Europe
Enter fullscreen mode Exit fullscreen mode

3. Server Cache (Backend)

Your server stores frequently accessed data in memory or on disk.

Database query: "Get all blog posts" → Takes 500ms
Cached result: Returns in 5ms
Enter fullscreen mode Exit fullscreen mode

4. Application Cache (Next.js Cache)

This is what we’re focusing on! Next.js has multiple caching layers that work together.

Key Caching Concepts You Need to Know

A. Cache Hit vs Cache Miss

  • Cache Hit : The data you need is already in the cache ✅ (Fast!)
  • Cache Miss : The data isn’t in cache, need to fetch it fresh ❌ (Slow)
// Example
function getUser(id: string) {
  const cached = cache.get(id);

  if (cached) {
    return cached; // ✅ CACHE HIT - Super fast!
  }

  // ❌ CACHE MISS - Need to fetch from database
  const user = database.query(`SELECT * FROM users WHERE id = ${id}`);
  cache.set(id, user); // Save for next time
  return user;
}
Enter fullscreen mode Exit fullscreen mode

B. Cache Invalidation

The hardest problem in computer science!

When data changes, you need to update or remove the old cached version. If you don’t, users see stale (outdated) data.

// Problem: User updates their profile
updateUserProfile(userId, newData);

// Now your cache has OLD data
// Users still see the old profile picture! 😱

// Solution: Invalidate the cache
cache.delete(userId); // Remove old data
// or
cache.set(userId, newData); // Update with fresh data
Enter fullscreen mode Exit fullscreen mode

C. Cache Lifetime / TTL (Time To Live)

How long should cached data stay fresh?

// Cache for 5 minutes
cache.set('products', data, { ttl: 300 }); // 300 seconds

// After 5 minutes, the cache automatically expires
// Next request will be a cache miss and fetch fresh data
Enter fullscreen mode Exit fullscreen mode

Different data needs different lifetimes:

Data Type Typical TTL Why? Stock prices 1 second Changes constantly Blog posts 1 hour — 1 day Changes rarely User profiles 5–15 minutes Changes occasionally Static images 1 year Almost never changes

D. Stale-While-Revalidate (SWR)

A smart caching strategy:

  1. Serve the cached (possibly stale) data immediately → User happy, fast response
  2. In the background, fetch fresh data
  3. Update the cache for next time
// User gets instant response with cached data (might be old)
return cachedData; 

// Meanwhile, in the background...
fetchFreshData().then(newData => {
  cache.set('key', newData); // Ready for next user
});
Enter fullscreen mode Exit fullscreen mode

This is like serving yesterday’s newspaper while someone runs to get today’s edition.

E. Prerendering vs Runtime Caching

Prerendering (Build-time caching):

  • Generate pages when you build your app
  • Super fast, but data might be outdated
  • Great for content that doesn’t change often

Runtime caching :

  • Generate pages when users request them
  • Always fresh, but slower
  • Great for personalized or frequently changing content

Part 2: How Next.js Handles Caching (The Big Picture)

Next.js is a React framework that helps you build fast websites. It has several caching mechanisms working together:

The Four Caching Layers in Next.js


┌─────────────────────────────────────────┐
│ 1. Request Memoization │ ← Same request = cached within one render
├─────────────────────────────────────────┤
│ 2. Data Cache │ ← Fetch requests cached across requests
├─────────────────────────────────────────┤
│ 3. Full Route Cache │ ← Entire pages cached (prerendered)
├─────────────────────────────────────────┤
│ 4. Router Cache (Client-side) │ ← Browser caches page transitions
└─────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Let’s understand each layer:

Layer 1: Request Memoization

What it does : Prevents duplicate requests during a single page render

// Component tree:
<Page>
  <Header>
    <UserMenu userId="123" /> ← Calls getUser("123")
  </Header>
  <Sidebar>
    <UserProfile userId="123" /> ← Calls getUser("123") again
  </Sidebar>
  <Content>
    <UserPosts userId="123" /> ← Calls getUser("123") third time!
  </Content>
</Page>

// Without memoization: 3 database calls for same user
// With memoization: 1 database call, result reused 3 times ✅
Enter fullscreen mode Exit fullscreen mode

Scope : Only lasts for the duration of ONE request When it clears : After the page finishes rendering

Layer 2: Data Cache

What it does : Caches fetch() responses across multiple requests

// First user visits your site
const products = await fetch('https://api.example.com/products');
// ↑ Fetches from API (slow), saves to cache

// Second user visits
const products = await fetch('https://api.example.com/products');
// ↑ Returns from cache (fast!) ⚡

// Third user, fourth user... all get cached data
Enter fullscreen mode Exit fullscreen mode

Scope : Across all requests, persists on server When it clears : Based on revalidation settings or manual invalidation

Layer 3: Full Route Cache

What it does : Saves the entire rendered HTML page

// Build time: Next.js prerenders your page
// Generates: /products → products.html

// When users visit /products:
// → Instantly serves products.html (no database calls, no React rendering)
// → Ultra fast! ⚡⚡⚡
Enter fullscreen mode Exit fullscreen mode

Scope : Production builds, CDN edge servers When it clears : When you redeploy or manually revalidate

Layer 4: Router Cache (Client-side)

What it does : Browser-side cache for navigation

// User visits: Home → Products → Contact → Products
// ↑
// Instant! Uses cached version
Enter fullscreen mode Exit fullscreen mode

Scope : User’s browser, during their session When it clears : Page refresh, or after 30 seconds (dynamic routes) / 5 minutes (static routes)

Part 3: Next.js 14 — The Aggressive Caching Era


Part 3 : Next Js 14

The Philosophy: Cache Everything by Default

In Next.js 14 and earlier, the philosophy was “Cache by default, opt-out if needed”.

This meant:

  • ✅ Every fetch() request was automatically cached forever
  • ✅ Pages were prerendered and cached
  • ✅ Everything was optimized for speed
  • ❌ But… this caused lots of confusion

How Caching Worked in Next.js 14

1. Fetch API — Cached Forever by Default

// This code in Next.js 14:
async function BlogPost() {
  const post = await fetch('https://api.example.com/posts/1');
  return <div>{post.title}</div>;
}
Enter fullscreen mode Exit fullscreen mode

What happens:

  1. First request: Fetches from API, saves to cache forever
  2. All future requests: Returns cached version
  3. IMPORTANT : Even if the API data changes, your cache won’t update!

This surprised many developers who expected their data to update automatically.

2. The Confusion: Stale Data Problem

// Day 1: You fetch blog posts
const posts = await fetch('https://api.example.com/posts');
// Returns: [Post A, Post B, Post C]
// ↓ CACHED

// Day 2: You publish a new blog post (Post D)
// Your API now has: [Post A, Post B, Post C, Post D]

// Day 3: User visits your site
const posts = await fetch('https://api.example.com/posts');
// Still returns: [Post A, Post B, Post C] ← OLD DATA! 😱
// Post D is missing because it's serving cached data
Enter fullscreen mode Exit fullscreen mode

This was Next.js 14’s biggest pain point!

Solutions in Next.js 14

Solution 1: Opt-out with cache: 'no-store'

// Tell Next.js: "Don't cache this!"
async function LiveData() {
  const data = await fetch('https://api.example.com/live-data', {
    cache: 'no-store' // ← Fresh data every time
  });
  return <div>{data.value}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Use when:

  • Stock prices, live scores, real-time data
  • User-specific data (shopping cart, profile)
  • Any data that changes frequently

Trade-off:

  • ✅ Always fresh data
  • ❌ Slower (every request hits the API)
  • ❌ More expensive (more API calls)

Solution 2: Time-based Revalidation (ISR — Incremental Static Regeneration)

// Cache for 60 seconds, then refresh
async function BlogPosts() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // ← Refresh every 60 seconds
  });
  return <PostList posts={posts} />;
}
Enter fullscreen mode Exit fullscreen mode

How it works:

Timeline:
0:00 → User A visits → Fetch fresh data → Cache it
0:30 → User B visits → Serve cached data (fast!)
0:45 → User C visits → Serve cached data (fast!)
1:01 → User D visits → Serve cached data BUT trigger background refresh
1:02 → Background refresh completes → Cache updated
1:30 → User E visits → Serve NEW cached data
Enter fullscreen mode Exit fullscreen mode

Use when:

  • Content updates periodically (news sites, blogs)
  • Balance between freshness and performance
  • Most common use case!

Choosing revalidation time:

  • Very dynamic (weather, stocks): 10–60 seconds
  • Moderately dynamic (news, social feeds): 5–15 minutes
  • Rarely changes (documentation, product catalog): 1 hour — 1 day

Solution 3: Route Segment Config

Control caching for an entire page:

// app/products/page.tsx

// Option A: Make entire page dynamic (no caching)
export const dynamic = 'force-dynamic';

// Option B: Set revalidation for entire page
export const revalidate = 3600; // 1 hour

// Option C: Force static (maximum caching)
export const dynamic = 'force-static';

export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products');
  return <ProductList products={products} />;
}
Enter fullscreen mode Exit fullscreen mode

What each option means:

Option Effect Use When dynamic = 'force-dynamic' No caching, always fresh User-specific pages, dashboards dynamic = 'force-static' Full caching, prerendered Marketing pages, blogs revalidate = N Cache for N seconds Most pages with semi-dynamic data

Solution 4: Manual Revalidation with Tags

// 1. Tag your cached data
async function getProducts() {
  const products = await fetch('https://api.example.com/products', {
    next: { 
      tags: ['products'] // ← Tag this cache entry
    }
  });
  return products;
}

// 2. Later, when products change, invalidate the cache
import { revalidateTag } from 'next/cache';

async function updateProduct(id: string) {
  await database.products.update(id);

  // Invalidate all caches tagged with 'products'
  revalidateTag('products'); // ← Cache cleared!
}
Enter fullscreen mode Exit fullscreen mode

Real-world example:

// E-commerce site
// Page 1: Product listing (tagged 'products')
// Page 2: Individual product (tagged 'products', 'product-123')
// Page 3: Shopping cart (tagged 'cart')

// Admin updates product 123
revalidateTag('product-123'); // Only update that product's cache

// Admin adds new product
revalidateTag('products'); // Update all product listings

// User checks out
revalidateTag('cart'); // Clear cart cache
Enter fullscreen mode Exit fullscreen mode

Cache Options Reference (Next.js 14)

// 1. Force Cache (default behavior in v14)
fetch(url, { cache: 'force-cache' })
// = Cache forever, never refresh

// 2. No Store
fetch(url, { cache: 'no-store' })
// = Never cache, always fetch fresh

// 3. No Cache
fetch(url, { cache: 'no-cache' })
// = Cache but always revalidate before using

// 4. Time-based Revalidation
fetch(url, { next: { revalidate: 60 } })
// = Cache for 60 seconds, then refresh

// 5. Tag-based Revalidation
fetch(url, { next: { tags: ['products'] } })
// = Cache until revalidateTag('products') is called
Enter fullscreen mode Exit fullscreen mode

Common Mistakes in Next.js 14

Mistake 1: Forgetting Data is Cached

// ❌ Problem
async function UserProfile({ userId }) {
  const user = await fetch(`https://api.example.com/users/${userId}`);
  return <div>{user.name}</div>;
}

// User updates their name in the database
// But your site still shows the OLD name! 😱
Enter fullscreen mode Exit fullscreen mode

Fix:

// ✅ Solution: Add revalidation
async function UserProfile({ userId }) {
  const user = await fetch(`https://api.example.com/users/${userId}`, {
    next: { revalidate: 300 } // Refresh every 5 minutes
  });
  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Caching User-Specific Data

// ❌ DANGER: Caching personal data
async function ShoppingCart({ userId }) {
  const cart = await fetch(`https://api.example.com/cart/${userId}`);
  // This is CACHED!
  // User A might see User B's shopping cart! 😱
  return <CartItems items={cart.items} />;
}
Enter fullscreen mode Exit fullscreen mode

Fix:

// ✅ Solution: Never cache user-specific data
async function ShoppingCart({ userId }) {
  const cart = await fetch(`https://api.example.com/cart/${userId}`, {
    cache: 'no-store' // Always fresh, always private
  });
  return <CartItems items={cart.items} />;
}
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Not Understanding ISR Behavior

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

async function NewsPage() {
  const articles = await fetch('https://api.example.com/news');
  return <ArticleList articles={articles} />;
}

// What developers think:
// "News updates every 60 seconds for everyone"

// What actually happens:
// 0:00 → User A visits → Fresh data → Cache it
// 0:30 → User B visits → Cached data (still fresh)
// 1:01 → User C visits → Cached data (now stale)
// BUT triggers background revalidation
// 1:02 → Revalidation complete → Cache updated
// 1:05 → User D visits → Sees NEW data

// User C saw old data even after 60 seconds!
// This is called "stale-while-revalidate"
Enter fullscreen mode Exit fullscreen mode

When Next.js 14 Worked Great

// Perfect use case: Documentation site
// Content rarely changes, speed is critical

export const revalidate = 86400; // Revalidate once per day

async function DocsPage({ slug }) {
  const doc = await fetch(`https://api.example.com/docs/${slug}`);
  return <Documentation content={doc.content} />;
}

// Results:
// ✅ Lightning fast (prerendered and cached)
// ✅ Updated daily (good enough for docs)
// ✅ Low server load (few API calls)
// ✅ Low costs (less bandwidth)
Enter fullscreen mode Exit fullscreen mode

Part 4: Next.js 15 — The Breaking Change


Part 4 : Next Js 15

The Problem That Led to Change

By 2024, Next.js developers were frustrated:

  1. Confusion : “Why isn’t my data updating?”
  2. Debugging nightmares : Hard to understand what was cached
  3. Surprising behavior : Defaults didn’t match expectations
  4. Over-caching : Too much implicit caching caused bugs

The Next.js team decided: “Let’s flip it around”

The Big Breaking Change

// NEXT.JS 14:
const data = await fetch('https://api.example.com/data');
// ↑ Cached forever by default

// NEXT.JS 15:
const data = await fetch('https://api.example.com/data');
// ↑ NOT cached by default (BREAKING CHANGE!)
Enter fullscreen mode Exit fullscreen mode

This was controversial but necessary:

  • ✅ Clearer behavior: Explicit > Implicit
  • ✅ Fewer surprises: Data is fresh by default
  • ❌ Migration pain: Existing apps need updates
  • ❌ Potentially slower: Need to add caching back

What Changed in Next.js 15

Change 1: Fetch is Uncached by Default

// Next.js 15
async function getData() {
  const res = await fetch('https://api.example.com/data');
  // ↑ This is NOT cached!
  // Every request hits the API
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

To cache, you must opt-in:

// Next.js 15 - Explicit caching
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache' // ← Now you must say "please cache this"
  });
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

Change 2: Route Segment Config Still Works

The old way of controlling caching still works:

// These still work in Next.js 15:
export const dynamic = 'force-static'; // Static rendering
export const dynamic = 'force-dynamic'; // Dynamic rendering
export const revalidate = 60; // Time-based revalidation
Enter fullscreen mode Exit fullscreen mode

This provides backward compatibility.

Change 3: Experimental “use cache” Directive

Next.js 15 introduced a new way to cache (experimental):

// next.config.js
module.exports = {
  experimental: {
    dynamicIO: true, // Enable new caching
  },
};

// Component with new caching
export async function ProductList() {
  'use cache'; // ← New directive!

  // Everything in this component is cached
  const products = await fetch('https://api.example.com/products');
  const categories = await db.categories.findAll();

  return (
    <div>
      {products.map(p => <Product key={p.id} {...p} />)}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What 'use cache' does:

  • Caches the entire component’s data fetching
  • Works with fetch AND database queries
  • More granular control than page-level caching

Partial Prerendering (PPR) — The Game Changer

Problem : Some parts of your page are static (e.g., header, footer), others are dynamic (e.g., user-specific content)

Old solution : Make entire page dynamic (slow) OR entire page static (not personalized)

New solution : Mix both!

// next.config.js
module.exports = {
  experimental: {
    ppr: true, // Enable Partial Prerendering
  },
};

// app/products/page.tsx
export default async function ProductsPage() {
  return (
    <div>
      {/* STATIC PART - Prerendered and cached */}
      <Header />
      <ProductCategories />
      <ProductFilters />

      {/* DYNAMIC PART - Generated at request time */}
      <Suspense fallback={<LoadingSkeleton />}>
        <PersonalizedRecommendations />
        <UserWishlist />
      </Suspense>

      {/* STATIC PART - Prerendered and cached */}
      <ProductList />
      <Footer />
    </div>
  );
}

async function PersonalizedRecommendations() {
  // This runs at REQUEST TIME (not cached)
  const session = await getSession();
  const recommendations = await fetch(
    `https://api.example.com/recommendations/${session.userId}`,
    { cache: 'no-store' } // Fresh data for each user
  );

  return <RecommendationsList items={recommendations} />;
}
Enter fullscreen mode Exit fullscreen mode

How PPR works:

Static Shell (cached):
┌─────────────────────────────────────┐
│ Header │
│ Categories │
│ ┌───────────────────────────────┐ │
│ │ [Dynamic Hole] │ │ ← Filled at request time
│ │ User-specific content │ │
│ └───────────────────────────────┘ │
│ Product Grid │
│ Footer │
└─────────────────────────────────────┘

Benefits:
✅ Fast initial load (shell is prerendered)
✅ Personalized content (dynamic parts)
✅ Best of both worlds!
Enter fullscreen mode Exit fullscreen mode

Migration from Next.js 14 to 15

Step 1: Audit Your Fetch Calls

// Find all fetch calls in your codebase
// Ask: "Should this be cached?"

// YES: Add cache: 'force-cache'
const staticData = await fetch(url, { cache: 'force-cache' });

// NO: Leave as is (uncached by default in v15)
const dynamicData = await fetch(url);
Enter fullscreen mode Exit fullscreen mode

Step 2: Use Route Config for Quick Fixes

// If an entire page should be static:
export const dynamic = 'force-static';

// This makes all fetch calls in this page behave like v14
Enter fullscreen mode Exit fullscreen mode

Step 3: Test Everything

// Before deploying to production:
// 1. Test with fresh data
// 2. Test with cached data
// 3. Test cache invalidation
// 4. Monitor your API call counts (should they increase?)
Enter fullscreen mode Exit fullscreen mode

Real-World Migration Example

// BEFORE (Next.js 14):
// app/blog/page.tsx
export default async function BlogPage() {
  // Cached forever by default
  const posts = await fetch('https://api.example.com/posts');
  return <PostList posts={posts} />;
}

// AFTER (Next.js 15) - Option 1: Explicit caching
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    cache: 'force-cache',
    next: { revalidate: 3600 } // Revalidate every hour
  });
  return <PostList posts={posts} />;
}

// AFTER (Next.js 15) - Option 2: Route config
export const revalidate = 3600; // Page-level revalidation

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts');
  return <PostList posts={posts} />;
}

// AFTER (Next.js 15) - Option 3: New 'use cache' directive
export default async function BlogPage() {
  return <CachedPosts />;
}

async function CachedPosts() {
  'use cache';
  const posts = await fetch('https://api.example.com/posts');
  return <PostList posts={posts} />;
}
Enter fullscreen mode Exit fullscreen mode

Part 5: Next.js 16 — Granular Control


Part 5 : Next JS 16

The Vision: Explicit, Predictable, Powerful

Next.js 16 takes the experimental features from v15 and makes them production-ready with better APIs and developer experience.

Philosophy :

  • Cache what you want, when you want
  • Fine-grained control at component/function level
  • Clear, explicit behavior

Key Features in Next.js 16

1. Stable “use cache” Directive

// next.config.js
const nextConfig = {
  experimental: {
    dynamicIO: true, // Enable 'use cache'
    cacheComponents: true, // New in v16
  },
};

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

What’s new in v16:

  • More stable implementation
  • Better error messages
  • TypeScript support improvements
  • Performance optimizations

2. Three Levels of Caching

Level 1: File-Level Caching

// utils/data.ts
'use cache'; // ← Everything in this file is cached

export async function getProducts() {
  const res = await fetch('https://api.example.com/products');
  return res.json();
}

export async function getCategories() {
  const res = await fetch('https://api.example.com/categories');
  return res.json();
}

export async function getBrands() {
  const res = await db.brands.findAll();
  return brands;
}

// All three functions are cached!
Enter fullscreen mode Exit fullscreen mode

When to use:

  • Utility files with data fetching functions
  • Shared data access layer
  • API client files

Level 2: Function-Level Caching

// More selective control
export async function getData() {
  // Cache this function
  'use cache';

  const products = await getProducts(); // Cached
  const user = await getCurrentUser(); // Also cached

  return { products, user };
}

// This function is NOT cached
export async function getRealtimeData() {
  const prices = await fetch('https://api.example.com/live-prices');
  return prices.json();
}
Enter fullscreen mode Exit fullscreen mode

When to use:

  • Mix cached and uncached functions in same file
  • Function-specific cache requirements
  • More granular than file-level

Level 3: Component-Level Caching

// app/products/page.tsx
export default function ProductsPage() {
  return (
    <div>
      <CachedHeader /> {/* Cached */}
      <CachedProductList /> {/* Cached */}
      <LivePriceDisplay /> {/* NOT cached */}
    </div>
  );
}

// Cached component
async function CachedHeader() {
  'use cache';

  const categories = await getCategories();
  return <Header categories={categories} />;
}

// Cached component
async function CachedProductList() {
  'use cache';

  const products = await getProducts();
  return <ProductGrid products={products} />;
}

// Dynamic component (no caching)
async function LivePriceDisplay() {
  const prices = await fetch('https://api.example.com/live-prices', {
    cache: 'no-store'
  });
  return <PriceChart data={prices} />;
}
Enter fullscreen mode Exit fullscreen mode

When to use:

  • Page has both static and dynamic parts
  • Maximum control over what’s cached
  • Combine with Suspense for best UX

3. Custom Cache Life Profiles

Define reusable caching strategies :

// next.config.js
const nextConfig = {
  experimental: {
    dynamicIO: true,
  },
  cacheLife: {
    // Profile 1: Very frequent updates
    realtime: {
      stale: 10, // Consider stale after 10 seconds
      revalidate: 20, // Revalidate after 20 seconds
      expire: 60, // Force expire after 60 seconds
    },

    // Profile 2: Moderate updates
    frequent: {
      stale: 60, // 1 minute
      revalidate: 120, // 2 minutes
      expire: 600, // 10 minutes
    },

    // Profile 3: Rare updates
    slow: {
      stale: 3600, // 1 hour
      revalidate: 7200, // 2 hours
      expire: 86400, // 24 hours
    },

    // Profile 4: Almost never changes
    permanent: {
      stale: 86400, // 1 day
      revalidate: 604800, // 1 week
      expire: 2592000, // 30 days
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Understanding stale, revalidate, expire:

Timeline for 'frequent' profile (stale: 60, revalidate: 120, expire: 600):

0:00 → Request comes in → Fetch fresh data → Cache it

0:30 → Request → Data is FRESH → Serve immediately
       └─ No background work needed

1:05 → Request → Data is STALE (>60s) but not expired
       ├─ Serve cached data immediately (fast for user!)
       └─ Trigger background revalidation

1:06 → Background revalidation completes → Update cache

2:00 → Request → Data is FRESH (was revalidated at 1:06)
       └─ Serve immediately

10:01 → Request → Data is EXPIRED (>600s)
        └─ MUST fetch fresh data (don't serve expired cache)

Why three values?
- stale: When to START thinking about refreshing (but still serve cached)
- revalidate: When to ACTUALLY refresh in background
- expire: HARD LIMIT - must fetch fresh, don't serve cached
Enter fullscreen mode Exit fullscreen mode

Using profiles in your code:

// Stock prices - need frequent updates
export async function StockPrice({ symbol }) {
  'use cache';
  cacheLife('realtime'); // Use 'realtime' profile

  const price = await fetch(`https://api.example.com/stock/${symbol}`);
  return <div>${price.current}</div>;
}

// Blog posts - update occasionally
export async function BlogList() {
  'use cache';
  cacheLife('slow'); // Use 'slow' profile

  const posts = await fetch('https://api.example.com/posts');
  return <PostGrid posts={posts} />;
}

// Legal documents - almost never change
export async function TermsOfService() {
  'use cache';
  cacheLife('permanent'); // Use 'permanent' profile

  const terms = await fetch('https://api.example.com/legal/terms');
  return <LegalDoc content={terms} />;
}
Enter fullscreen mode Exit fullscreen mode

4. Cache Tags — Powerful Invalidation

The Problem : How do you invalidate cache when data changes?

// You update a product in admin panel
await db.products.update(productId, newData);

// But users still see old product info! 😱
// Need to clear the cache
Enter fullscreen mode Exit fullscreen mode

The Solution : Tag your caches and invalidate by tag

// Step 1: Tag your cached data
export async function getProducts() {
  'use cache';
  cacheTag('products'); // ← Tag this cache

  const products = await fetch('https://api.example.com/products');
  return products.json();
}

export async function getProduct(id: string) {
  'use cache';
  cacheTag('products', `product-${id}`); // ← Multiple tags!

  const product = await fetch(`https://api.example.com/products/${id}`);
  return product.json();
}

// Step 2: Invalidate when data changes
import { revalidateTag } from 'next/cache';

export async function updateProduct(id: string, data: ProductData) {
  // Update in database
  await db.products.update(id, data);

  // Invalidate caches
  revalidateTag(`product-${id}`); // Clear this product's cache
  revalidateTag('products'); // Clear product list cache

  // Now users see fresh data! ✅
}
Enter fullscreen mode Exit fullscreen mode

Advanced tagging strategies:

// E-commerce example with hierarchical tags

// Category page
async function CategoryPage({ categoryId }) {
  'use cache';
  cacheTag('products', `category-${categoryId}`);

  const products = await getProductsByCategory(categoryId);
  return <ProductGrid products={products} />;
}

// Product detail page
async function ProductPage({ productId }) {
  'use cache';
  cacheTag('products', `product-${productId}`);

  const product = await getProduct(productId);
  return <ProductDetails product={product} />;
}

// Invalidation scenarios:

// Scenario 1: Admin updates single product
revalidateTag(`product-${id}`);
// ↑ Only affects that product's page

// Scenario 2: Admin updates product and it changes category
revalidateTag(`product-${id}`);
revalidateTag(`category-${oldCategoryId}`);
revalidateTag(`category-${newCategoryId}`);
// ↑ Updates product page + both category pages

// Scenario 3: Price changes for entire category (e.g., sale)
revalidateTag(`category-${categoryId}`);
// ↑ All products in category refresh

// Scenario 4: Global inventory update
revalidateTag('products');
// ↑ Clears ALL product-related caches
Enter fullscreen mode Exit fullscreen mode

5. New Revalidation APIs

Next.js 16 adds more powerful cache control:

revalidateTag() - Background Revalidation

import { revalidateTag } from 'next/cache';

export async function updatePost(id: string) {
  await db.posts.update(id);

  // Revalidate in background
  revalidateTag('posts');
  // ↑ Current users see old data
  // Next users see fresh data
}
Enter fullscreen mode Exit fullscreen mode

revalidateTag() with Profile - Custom Revalidation

// Revalidate with specific cache profile
revalidateTag('posts', 'frequent');
// ↑ Use 'frequent' profile settings for revalidation
Enter fullscreen mode Exit fullscreen mode

updateTag() - Immediate Update (NEW in v16!)

import { updateTag } from 'next/cache';

export async function urgentUpdate(id: string) {
  await db.posts.update(id);

  // IMMEDIATELY invalidate (not background)
  updateTag('posts');
  // ↑ All current users will get fresh data on next request
  // More aggressive than revalidateTag
}
Enter fullscreen mode Exit fullscreen mode

When to use each:

API Behavior Use When revalidateTag() Background revalidation Normal updates, gradual rollout revalidateTag(tag, profile) Background with custom timing Need specific cache behavior updateTag() Immediate invalidation Critical updates, security fixes

6. Turbopack Development Cache

Speed up local development:

// next.config.js
const nextConfig = {
  experimental: {
    turbopackFileSystemCacheForDev: true,
  },
};
Enter fullscreen mode Exit fullscreen mode

What it does:

  • Saves compiled files to disk
  • Faster server restarts
  • Persists across dev sessions
Without cache:
npm run dev → Compiles everything (30 seconds) 😴

With cache:
npm run dev → Loads from cache (3 seconds) ⚡

Code change → Only recompiles changed files
Enter fullscreen mode Exit fullscreen mode

Complete Next.js 16 Example: E-commerce Site

Let’s build a realistic e-commerce page with optimal caching:

// next.config.js
const nextConfig = {
  experimental: {
    dynamicIO: true,
    cacheComponents: true,
  },
  cacheLife: {
    realtime: { stale: 10, revalidate: 30, expire: 60 },
    frequent: { stale: 60, revalidate: 300, expire: 3600 },
    slow: { stale: 3600, revalidate: 7200, expire: 86400 },
  },
};

export default nextConfig;

// app/products/[id]/page.tsx
import { Suspense } from 'react';

export default function ProductPage({ params }) {
  return (
    <div>
      {/* Cached: Basic product info */}
      <ProductDetails productId={params.id} />

      {/* Cached: Product reviews */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>

      {/* Cached: Related products */}
      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedProducts productId={params.id} />
      </Suspense>

      {/* NOT cached: Real-time inventory */}
      <Suspense fallback={<InventorySkeleton />}>
        <InventoryStatus productId={params.id} />
      </Suspense>

      {/* NOT cached: Personalized recommendations */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <PersonalizedRecommendations productId={params.id} />
      </Suspense>
    </div>
  );
}

// Component 1: Product Details (cached, slow updates)
async function ProductDetails({ productId }) {
  'use cache';
  cacheLife('slow'); // Cache for 24 hours
  cacheTag('products', `product-${productId}`);

  const product = await db.products.findOne({ id: productId });

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <div>Base Price: ${product.basePrice}</div>
    </div>
  );
}

// Component 2: Product Reviews (cached, moderate updates)
async function ProductReviews({ productId }) {
  'use cache';
  cacheLife('frequent'); // Cache for 1 hour
  cacheTag('reviews', `product-${productId}-reviews`);

  const reviews = await db.reviews.findMany({
    where: { productId },
    orderBy: { createdAt: 'desc' },
    take: 10,
  });

  return (
    <div>
      <h2>Customer Reviews</h2>
      {reviews.map(review => (
        <ReviewCard key={review.id} review={review} />
      ))}
    </div>
  );
}

// Component 3: Related Products (cached, slow updates)
async function RelatedProducts({ productId }) {
  'use cache';
  cacheLife('slow');
  cacheTag('products', `product-${productId}-related`);

  const related = await db.products.findRelated(productId);

  return (
    <div>
      <h2>You Might Also Like</h2>
      <ProductGrid products={related} />
    </div>
  );
}

// Component 4: Inventory Status (NOT cached, real-time)
async function InventoryStatus({ productId }) {
  // No 'use cache' - always fresh data

  const inventory = await fetch(
    `https://api.inventory.com/stock/${productId}`,
    { cache: 'no-store' } // Always fetch fresh
  );

  const stock = await inventory.json();

  return (
    <div>
      {stock.available ? (
        <span className="text-green-600">
          {stock.quantity} in stock - Ships today!
        </span>
      ) : (
        <span className="text-red-600">Out of stock</span>
      )}
    </div>
  );
}

// Component 5: Personalized Recommendations (NOT cached, user-specific)
async function PersonalizedRecommendations({ productId }) {
  // No caching - user-specific data

  const session = await getSession();

  const recommendations = await fetch(
    `https://api.recommendations.com/user/${session.userId}/context/${productId}`,
    { cache: 'no-store' }
  );

  const items = await recommendations.json();

  return (
    <div>
      <h2>Recommended for You</h2>
      <ProductCarousel products={items} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Cache invalidation for the e-commerce site:

// actions/products.ts
'use server';

import { revalidateTag, updateTag } from 'next/cache';

// Action 1: Admin updates product details
export async function updateProduct(id: string, data: ProductData) {
  await db.products.update(id, data);

  // Invalidate product cache
  revalidateTag(`product-${id}`);
  revalidateTag(`product-${id}-related`);
  revalidateTag('products'); // Product listings
}

// Action 2: User submits review
export async function submitReview(productId: string, review: ReviewData) {
  await db.reviews.create({ productId, ...review });

  // Invalidate reviews cache for this product
  revalidateTag(`product-${productId}-reviews`);
}

// Action 3: Critical price update (Black Friday sale!)
export async function updatePrices(productIds: string[]) {
  await db.products.updateMany(productIds, { sale: true });

  // IMMEDIATE invalidation for all products
  updateTag('products'); // More aggressive than revalidateTag

  // Also clear related caches
  productIds.forEach(id => {
    updateTag(`product-${id}`);
  });
}

// Action 4: Inventory updated (handled by external service)
// No cache invalidation needed - inventory is never cached!
Enter fullscreen mode Exit fullscreen mode

Performance Benefits

Traditional approach (everything dynamic):
- Product page: 800ms load time
- API calls per page view: 5
- Database queries per page view: 8
- Server CPU usage: High

With Next.js 16 caching:
- Product page: 150ms load time (cached parts instant)
- API calls per page view: 2 (only uncached parts)
- Database queries per page view: 2 (only fresh data)
- Server CPU usage: Low

Result:
✅ 5x faster
✅ 80% fewer API calls
✅ 75% fewer database queries
✅ Lower hosting costs
✅ Better user experience
Enter fullscreen mode Exit fullscreen mode

Part 6: Advanced Caching Patterns


Part 6: Advanced Caching Patterns (Pro Techniques)

Pattern 1: Hierarchical Caching

Cache data at multiple levels with different lifetimes:

// Level 1: Master product catalog (changes rarely)
async function getProductCatalog() {
  'use cache';
  cacheLife('slow'); // 24 hour cache
  cacheTag('catalog');

  return await db.products.findAll();
}

// Level 2: Category products (changes occasionally)
async function getCategoryProducts(categoryId: string) {
  'use cache';
  cacheLife('frequent'); // 1 hour cache
  cacheTag('catalog', `category-${categoryId}`);

  return await db.products.findMany({
    where: { categoryId }
  });
}

// Level 3: Individual product (changes more often)
async function getProduct(id: string) {
  'use cache';
  cacheLife('realtime'); // 1 minute cache
  cacheTag('catalog', `product-${id}`);

  return await db.products.findOne({ id });
}

// When product updates:
revalidateTag(`product-${id}`); // Clears product cache (1 min)
revalidateTag(`category-${catId}`); // Clears category cache (1 hour)
// Catalog cache remains (24 hours) unless explicitly cleared
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Optimistic Caching

Update cache immediately while background process completes:

'use server';

export async function likePost(postId: string) {
  // 1. Update cache optimistically
  const post = await getPost(postId);
  const updatedPost = { ...post, likes: post.likes + 1 };

  // 2. Update in background
  db.posts.update(postId, { likes: updatedPost.likes })
    .then(() => {
      // 3. Revalidate to ensure consistency
      revalidateTag(`post-${postId}`);
    });

  // Return immediately with optimistic data
  return updatedPost;
}
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Cache Warming

Pre-populate cache before users need it:

// Build time: Warm frequently accessed caches
export async function warmCache() {
  // Warm top 100 products
  const topProducts = await db.products.findMany({
    orderBy: { views: 'desc' },
    take: 100,
  });

  for (const product of topProducts) {
    // These will be cached on first call
    await getProduct(product.id);
    await getProductReviews(product.id);
  }
}

// Run during deployment
// Now first users get instant cached responses!
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Cache Segmentation by User

Different cache behavior for different users:

async function getDashboard() {
  const session = await getSession();

  if (session.userType === 'premium') {
    'use cache';
    cacheLife('realtime'); // Premium users get fresher data
    cacheTag(`dashboard-${session.userId}`);
  }

  // Free users get older cache or no cache
  const data = await fetch('https://api.example.com/dashboard');
  return data.json();
}
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Conditional Caching

Cache based on runtime conditions:

async function getProducts({ filters, sortBy }) {
  // Only cache common filter combinations
  const isCommonQuery = filters.length === 0 && sortBy === 'popular';

  if (isCommonQuery) {
    'use cache';
    cacheLife('frequent');
    cacheTag('products');
  }

  // Custom queries aren't cached (too many permutations)
  const products = await db.products.findMany({ filters, sortBy });
  return products;
}
Enter fullscreen mode Exit fullscreen mode

Part 7: Debugging and Monitoring Caching


Part 7: Debugging and Monitoring Caching Tools & Techniques

How to Debug Cache Issues

1. Check if Data is Cached

// Add logging to your cached functions
async function getProducts() {
  'use cache';

  console.log('[CACHE] Fetching products - if you see this, cache missed!');

  const products = await fetch('https://api.example.com/products');
  return products.json();
}

// In browser console:
// If you see the log ONCE: Cache working ✅
// If you see the log MULTIPLE times: Cache not working ❌
Enter fullscreen mode Exit fullscreen mode

2. Use Next.js DevTools

# Install Next.js DevTools
npm install @next/dev-tools

# In your app:
import { DevTools } from '@next/dev-tools';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <DevTools />
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

DevTools shows:

  • Cache hits/misses
  • Cache size
  • Revalidation events
  • Performance metrics

3. Add Cache Headers for Debugging

// middleware.ts
export function middleware(request: Request) {
  const response = NextResponse.next();

  // Add custom header to see cache status
  response.headers.set('X-Cache-Status', 'HIT' or 'MISS');
  response.headers.set('X-Cache-Age', cacheAge.toString());

  return response;
}
Enter fullscreen mode Exit fullscreen mode

Check in browser DevTools → Network tab → Response Headers

Common Cache Problems and Solutions

Problem 1: Data Not Updating

// ❌ Problem
async function getData() {
  'use cache';
  // No revalidation strategy!
  return await fetch(url);
}

// ✅ Solution: Add cache lifetime
async function getData() {
  'use cache';
  cacheLife('frequent'); // Will revalidate
  cacheTag('data'); // Can manually invalidate
  return await fetch(url);
}
Enter fullscreen mode Exit fullscreen mode

Problem 2: Cache Too Aggressive

// ❌ Problem: Caching user-specific data
async function getUserDashboard() {
  'use cache'; // DANGER: Might show User A's data to User B!
  const session = await getSession();
  return await fetchDashboard(session.userId);
}

// ✅ Solution: Don't cache user-specific data
async function getUserDashboard() {
  // No caching for personal data
  const session = await getSession();
  return await fetchDashboard(session.userId);
}
Enter fullscreen mode Exit fullscreen mode

Problem 3: Cache Stampede

Multiple requests hit uncached data simultaneously:

100 users visit at same time
→ All trigger database query
→ Database overloaded!

// ✅ Solution: Use request deduplication
import { cache } from 'react';

const getProducts = cache(async () => {
  // React will deduplicate this during render
  return await db.products.findAll();
});

// Even if called 100 times in one render,
// only executes ONCE
Enter fullscreen mode Exit fullscreen mode

Problem 4: Memory Leaks

Too much caching consumes server memory:

// ❌ Problem: Infinite cache growth
async function searchProducts(query: string) {
  'use cache';
  // Every unique query creates new cache entry
  // queries grow infinitely!
  return await db.products.search(query);
}

// ✅ Solution: Don't cache high-cardinality data
async function searchProducts(query: string) {
  // Don't cache search results
  return await db.products.search(query);
}

// OR: Cache only common searches
async function searchProducts(query: string) {
  const popularQueries = ['laptop', 'phone', 'tablet'];

  if (popularQueries.includes(query)) {
    'use cache';
    cacheTag('search');
  }

  return await db.products.search(query);
}
Enter fullscreen mode Exit fullscreen mode

Monitoring Cache Performance

Key Metrics to Track

  1. Cache Hit Rate
Cache Hit Rate = (Cache Hits / Total Requests) × 100%

Good: >80%
Okay: 60-80%
Bad: <60%
Enter fullscreen mode Exit fullscreen mode
  1. Cache Size
// Monitor memory usage
console.log('Cache size:', process.memoryUsage().heapUsed);

// Alert if too large:
if (cacheSize > MAX_CACHE_SIZE) {
  // Clear some caches
  revalidateTag('old-data');
}
Enter fullscreen mode Exit fullscreen mode
  1. Revalidation Frequency
Too frequent: Cache not effective
Too rare: Stale data problems
Sweet spot: Depends on data volatility
Enter fullscreen mode Exit fullscreen mode
  1. Response Times
Cached response: <50ms ✅
Uncached response: 200-500ms
Database query: 100-300ms

If cached responses are slow, something's wrong!
Enter fullscreen mode Exit fullscreen mode

Part 8: Best Practices and Decision Framework

Decision Tree: Should You Cache This Data?

START: Need to fetch data

1. Is this user-specific? (cart, profile, preferences)
   YES → DON'T CACHE ❌
   NO → Continue

2. Does it change in real-time? (stock prices, live scores)
   YES → DON'T CACHE ❌
   NO → Continue

3. Does it contain sensitive information? (passwords, payment info)
   YES → DON'T CACHE ❌
   NO → Continue

4. How often does it change?
   Every second → Cache for 1-10 seconds (or don't cache)
   Every minute → Cache for 1-5 minutes
   Every hour → Cache for 15-60 minutes
   Every day → Cache for 1-24 hours
   Rarely → Cache for days/weeks

5. How important is freshness?
   Critical (prices, inventory) → Short cache or no cache
   Important (news, posts) → Medium cache (minutes-hours)
   Not critical (docs, legal) → Long cache (days)

6. How expensive is it to fetch?
   Very expensive → Cache longer
   Cheap → Cache shorter or don't cache

CACHE IT ✅
Enter fullscreen mode Exit fullscreen mode

Caching Strategy by Use Case

E-commerce

// Product catalog: Cache 1-24 hours
async function getProducts() {
  'use cache';
  cacheLife('slow');
  cacheTag('products');
}

// Product prices: Cache 5-15 minutes
async function getProductPrices() {
  'use cache';
  cacheLife('frequent');
  cacheTag('prices');
}

// Inventory: Don't cache (real-time)
async function getInventory(productId: string) {
  // No cache
  return await fetch(`/api/inventory/${productId}`, {
    cache: 'no-store'
  });
}

// Shopping cart: Don't cache (user-specific)
async function getCart(userId: string) {
  // No cache
  return await db.carts.findOne({ userId });
}
Enter fullscreen mode Exit fullscreen mode

News/Blog Site

// Articles: Cache 15 minutes - 1 hour
async function getArticles() {
  'use cache';
  cacheLife('frequent');
  cacheTag('articles');
}

// Comments: Cache 5 minutes
async function getComments(articleId: string) {
  'use cache';
  cacheLife('realtime');
  cacheTag('comments', `article-${articleId}`);
}

// Trending topics: Cache 1 minute
async function getTrending() {
  'use cache';
  cacheLife('realtime');
  cacheTag('trending');
}
Enter fullscreen mode Exit fullscreen mode

SaaS Dashboard

// Analytics (yesterday): Cache 24 hours
async function getYesterdayAnalytics() {
  'use cache';
  cacheLife('slow');
  cacheTag('analytics');
}

// Analytics (today): Cache 5 minutes
async function getTodayAnalytics() {
  'use cache';
  cacheLife('frequent');
  cacheTag('analytics-today');
}

// Live metrics: Don't cache
async function getLiveMetrics() {
  // No cache - real-time data
}

// User settings: Don't cache (user-specific)
async function getUserSettings(userId: string) {
  // No cache - personal data
}
Enter fullscreen mode Exit fullscreen mode

The Golden Rules of Caching

  1. Never cache user-specific data unless you really know what you’re doing
  2. Never cache sensitive information (passwords, payment info, personal data)
  3. Cache what’s expensive to fetch (complex DB queries, external APIs)
  4. Match cache lifetime to data volatility (fast-changing = short cache)
  5. Always have a way to invalidate (use cache tags)
  6. Monitor your cache performance (hit rates, response times)
  7. Test with fresh AND stale data (don’t forget edge cases)
  8. Document your caching strategy (teammates need to understand it)

Part 9: Quick Reference

Next.js 14 Quick Reference

// Default: Everything cached
const data = await fetch(url); // Cached forever

// Opt-out: Disable caching
const data = await fetch(url, { cache: 'no-store' });

// Time-based: Revalidate periodically
const data = await fetch(url, { next: { revalidate: 60 } });

// Page-level: Control entire route
export const revalidate = 60;
export const dynamic = 'force-dynamic';

// Tag-based: Manual invalidation
const data = await fetch(url, { next: { tags: ['products'] } });
revalidateTag('products');
Enter fullscreen mode Exit fullscreen mode

Next.js 15 Quick Reference

// Default: NOT cached (breaking change!)
const data = await fetch(url); // Uncached

// Opt-in: Enable caching
const data = await fetch(url, { cache: 'force-cache' });

// Still works: Route segment config
export const dynamic = 'force-static';
export const revalidate = 60;

// Experimental: 'use cache' directive
async function Component() {
  'use cache';
  // Cached
}

// Experimental: Partial Prerendering
<Suspense>
  <DynamicContent />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

Next.js 16 Quick Reference

// Enable features
// next.config.js
experimental: {
  dynamicIO: true,
  cacheComponents: true,
}

// File-level caching
'use cache'; // Top of file

// Function-level caching
async function getData() {
  'use cache';
}

// Component-level caching
async function Component() {
  'use cache';
}

// Cache profiles
cacheLife: {
  frequent: { stale: 60, revalidate: 120, expire: 600 }
}
// Use in code:
cacheLife('frequent');

// Cache tags
cacheTag('products', 'product-123');
revalidateTag('products');
updateTag('products'); // Immediate invalidation
Enter fullscreen mode Exit fullscreen mode

Migration Cheat Sheet

// FROM Next.js 14
const data = await fetch(url);
// TO Next.js 15
const data = await fetch(url, { cache: 'force-cache' });

// FROM Next.js 15
export const revalidate = 60;
// TO Next.js 16
async function getData() {
  'use cache';
  cacheLife('frequent');
}

// FROM Next.js 14/15
revalidateTag('products');
// TO Next.js 16 (immediate)
updateTag('products');
Enter fullscreen mode Exit fullscreen mode

Conclusion

Caching in Next.js has evolved significantly:

  • Next.js 14 : Cached by default (implicit)
  • Next.js 15 : Uncached by default (breaking change)
  • Next.js 16 : Granular opt-in caching (explicit)

The journey shows a clear trend: explicit > implicit

Key Takeaways

  1. Understand your data : Is it static, dynamic, or user-specific?
  2. Match cache strategy to data : Volatile data = short cache
  3. Use the right tools : Tags, profiles, revalidation
  4. Monitor and measure : Track cache hit rates
  5. Always test : Fresh data, stale data, edge cases

What’s Next?

Next.js will likely continue to:

  • Make caching more predictable
  • Provide better tooling and debugging
  • Offer more granular control
  • Improve performance defaults

The future is explicit, predictable caching where you decide exactly what gets cached and when.

Additional Resources

Last updated: January 2026 Written for absolute beginners learning Next.js caching

Happy caching! 🚀

Top comments (0)