Next.js has undergone significant caching philosophy changes. Version 14 aggressively cached by default, v15 made everything dynamic by default, and now with Cache Components (introduced in v15 canary and expanded in v16), we have explicit opt-in caching with the use cache directive. Understanding this evolution is crucial for modern Next.js development.
The Three Eras of Next.js Caching
v14 Era: Aggressive caching by default
v15 Era: Dynamic by default, opt into caching
Cache Components Era: Explicit caching with use cache directive
Part 1: The v14 → v15 Shift
v14: Cache by Default
In Next.js 14, fetch requests were automatically cached with force-cache as the default behavior, meaning data was cached indefinitely unless explicitly opted out.
// Next.js 14 - Cached by default
export default async function Page() {
const res = await fetch('https://api.example.com/products');
const products = await res.json();
// This was CACHED indefinitely
return <ProductList products={products} />;
}
// v14: Opting out of cache
const res = await fetch('https://api.example.com/products', {
cache: 'no-store' // Explicitly disable caching
});
v15: Dynamic by Default
Next.js 15 changed the default behavior so fetch requests are not cached by default. You must explicitly opt into caching.
// Next.js 15 - NOT cached by default
export default async function Page() {
const res = await fetch('https://api.example.com/products');
const products = await res.json();
// Fresh data on every request
return <ProductList products={products} />;
}
// v15: Opting into cache
const res = await fetch('https://api.example.com/products', {
cache: 'force-cache' // Explicitly enable caching
});
Part 2: Understanding the Four Caching Layers
Next.js operates with four distinct caching mechanisms that work together. Understanding their interaction is essential.
Layer 1: Request Memoization
Request memoization happens automatically within a single render pass, deduplicating identical fetch requests during rendering.
// Multiple components making the same request
async function Header() {
const user = await fetch('/api/user'); // Request 1
return <div>{user.name}</div>;
}
async function Sidebar() {
const user = await fetch('/api/user'); // Deduplicated automatically
return <nav>{user.email}</nav>;
}
export default function Page() {
return (
<>
<Header />
<Sidebar />
</> // Only ONE actual network request
);
}
Scope: Single request/render cycle
Duration: Until response is sent
Control: Automatic (React optimization)
Version changes: No change between v14, v15, or v16
Layer 2: Data Cache (The Major Change)
The Data Cache persists fetch results across incoming server requests and deployments. This is where v14 and v15 differ fundamentally.
v14 behavior:
// Cached indefinitely by default
const res = await fetch('/api/products');
// To make it dynamic, add:
const res = await fetch('/api/products', { cache: 'no-store' });
v15 behavior:
// Dynamic by default
const res = await fetch('/api/products');
// To cache it, add:
const res = await fetch('/api/products', {
cache: 'force-cache'
});
// Or with time-based revalidation:
const res = await fetch('/api/products', {
next: { revalidate: 3600 } // Revalidate every hour
});
Layer 3: Full Route Cache
The Full Route Cache stores rendered HTML and RSC payload for static routes. Routes are statically rendered at build time or dynamically at request time.
// v15: Routes are dynamic by default
export default async function Page() {
const data = await getData();
return <div>{data}</div>;
// Rendered on every request
}
// Opt into static generation
export const dynamic = 'force-static';
export default async function Page() {
const data = await getData();
return <div>{data}</div>;
// Rendered once at build time
}
Configuration options:
-
dynamic = 'auto'- Automatic (default in v15) -
dynamic = 'force-static'- Force static generation -
dynamic = 'force-dynamic'- Force dynamic rendering -
dynamic = 'error'- Error if dynamic functions used
Layer 4: Router Cache (Client-Side)
The Router Cache stores RSC payload in the browser during navigation, and as of Next.js 15, page segments are opted out by default.
v14: Cached for 30 seconds (dynamic), 5 minutes (static)
v15: 0 seconds by default (effectively disabled for dynamic routes)
// next.config.js - Configure client cache in v15
module.exports = {
experimental: {
staleTimes: {
dynamic: 30, // 30 seconds for dynamic routes
static: 180, // 3 minutes for static routes
},
},
};
Part 3: Cache Components with use cache
Cache Components center around the new use cache directive, making caching entirely opt-in and more explicit than previous versions.
Enabling Cache Components
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
Basic Usage
The use cache directive can be applied at the page, component, or function level:
// Page-level caching
export default async function Page() {
'use cache';
const random = Math.random();
const now = Date.now();
return (
<div>
<p>{random}</p>
<p>{now}</p>
</div>
);
// All requests will see the same values until revalidated
}
// Component-level caching
export async function ProductList() {
'use cache';
const products = await fetch('/api/products').then(r => r.json());
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// Function-level caching
export async function getProducts() {
'use cache';
const res = await fetch('/api/products');
return res.json();
}
Using cacheLife Profiles
By default, use cache uses the default profile with 5 minutes client-side stale time, 15 minutes server-side revalidate, and never expires.
import { cacheLife } from 'next/cache';
export async function getData() {
'use cache';
cacheLife('hours'); // Use built-in 'hours' profile
return fetch('/api/data').then(r => r.json());
}
// Define custom profiles in next.config.ts
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
biweekly: {
stale: 60 * 60 * 24 * 14, // 14 days
revalidate: 60 * 60 * 24, // 1 day
expire: 60 * 60 * 24 * 14, // 14 days
},
},
};
Cache Invalidation
Use cacheTag to tag cached data and revalidateTag to invalidate it on-demand:
import { cacheTag, updateTag, revalidateTag } from 'next/cache';
// Tag the cache
export async function getProducts() {
'use cache';
cacheTag('products');
return db.query('SELECT * FROM products');
}
// Invalidate immediately (Server Actions)
export async function updateProduct(id: string) {
'use server';
await db.products.update({ id });
updateTag('products'); // Immediate cache expiration
}
// Invalidate with stale-while-revalidate
export async function refreshProducts() {
await someUpdate();
revalidateTag('products', 'max'); // Recommended approach
}
Migration Guide
From v14 to v15
Step 1: Identify and remove unnecessary cache configurations
// v14 - Remove these in v15 (now default):
{ cache: 'no-store' }
{ next: { revalidate: 0 } }
export const revalidate = 0;
// v15 - Keep/add these:
{ cache: 'force-cache' }
{ next: { revalidate: 60 } }
export const dynamic = 'force-static';
Step 2: Add caching where beneficial
// Static data - aggressive caching
const config = await fetch('/api/config', {
cache: 'force-cache'
});
// Semi-dynamic - time-based revalidation
const products = await fetch('/api/products', {
next: { revalidate: 300 } // 5 minutes
});
// User-specific - keep dynamic (no config needed)
const userData = await fetch('/api/user');
Migrating to Cache Components
// Before: v15 with fetch caching
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
}).then(r => r.json());
return <div>{data}</div>;
}
// After: Cache Components approach
import { cacheLife } from 'next/cache';
export default async function Page() {
'use cache';
cacheLife('hours');
const data = await fetch('https://api.example.com/data')
.then(r => r.json());
return <div>{data}</div>;
}
Real-World Patterns
Pattern 1: Hybrid E-commerce Page
export default async function ProductPage({ params }) {
// Categories rarely change - cache aggressively
const categories = await fetch('/api/categories', {
next: { revalidate: 86400 } // 24 hours
}).then(r => r.json());
// Product data changes moderately
const product = await fetch(`/api/products/${params.id}`, {
next: { revalidate: 300 } // 5 minutes
}).then(r => r.json());
// Stock levels change frequently - keep dynamic
const stock = await fetch(`/api/stock/${params.id}`)
.then(r => r.json());
return <ProductView product={product} stock={stock} categories={categories} />;
}
Pattern 2: Static Marketing with Dynamic Pricing
export default async function LandingPage() {
'use cache';
cacheLife('weeks');
// Static content cached for weeks
return (
<>
<Hero />
<Features />
<Suspense fallback={<div>Loading pricing...</div>}>
<DynamicPricing />
</Suspense>
</>
);
}
// Dynamic pricing without suspense wrapper
async function DynamicPricing() {
const prices = await fetch('/api/pricing').then(r => r.json());
return <PricingTable prices={prices} />;
}
Decision Matrix
| Data Type | v15 Strategy | Cache Components |
|---|---|---|
| Config/Settings | cache: 'force-cache' |
'use cache' + cacheLife('max')
|
| Marketing Content | dynamic = 'force-static' |
'use cache' at page level |
| Product Catalog | revalidate: 300 |
'use cache' + cacheLife('minutes')
|
| User Dashboards | Default (dynamic) | Wrap in <Suspense>
|
| Real-time Data | Default (dynamic) | No caching |
Performance Considerations
Although fetch requests are not cached by default in v15, Next.js will pre-render routes and cache the HTML for improved performance.
When to use each approach:
- Use v15 fetch caching for simple, straightforward caching needs
-
Use Cache Components when you need:
- Caching beyond fetch (database queries, file operations)
- Fine-grained cache invalidation with tags
- Partial prerendering with dynamic sections
- Complex cache lifetime management
Key Takeaways
- v14 → v15: Default changed from cached to dynamic for fetch requests
- Four layers: Request Memoization, Data Cache, Full Route Cache, Router Cache
-
Cache Components: New explicit opt-in model with
use cachedirective -
Migration path: Remove
cache: 'no-store', addcache: 'force-cache'where needed - Modern approach: Cache strategically, not by default
The evolution from v14's aggressive caching to v15's dynamic defaults and now Cache Components represents Next.js maturing toward explicit, predictable caching behavior that developers can reason about clearly.
Top comments (0)