本系列文章将围绕Next.js技术栈,旨在为AI Agent开发者提供一套完整的客户端侧工程实践指南。
在《Next.js数据获取与缓存策略》一章我们已经简单的介绍了Next.js的缓存策略。 本文将更深入剖析 Next.js 的多层缓存架构,揭示其工作原理、最佳实践以及常见陷阱,并提供完整的实战案例。
一、为什么理解缓存至关重要
在现代 Web 应用中,缓存是性能优化的核心。Next.js 作为 React 全栈框架,提供了一套复杂而强大的多层缓存系统。然而,这也带来了理解上的挑战:
- 多层缓存如何协同工作?
- 何时数据会被缓存?何时会重新获取?
- 如何精确控制缓存行为?
- 常见的缓存陷阱有哪些?
不理解这些机制,可能导致:
- ❌ 用户看到过期数据
- ❌ 不必要的服务器负载
- ❌ 难以调试的 stale-while-revalidate 问题
- ❌ 内存泄漏或缓存爆炸
本文将通过理论 + 代码 + 图示的方式,彻底揭开 Next.js 缓存的神秘面纱。
二、Next.js 缓存架构全景图
1. 四层缓存架构图
Next.js 13+ (App Router) 实现了四层缓存架构,每一层都有不同的职责和生命周期:
- 存储已访问页面的 RSC Payload
- 默认 TTL: 5分钟 (静态) / 即时失效(动态)
"] end subgraph Edge ["☁️ CDN / 边缘层"] FullRouteCache["【3. Full Route Cache(完整路由缓存)】
- 缓存整个页面的 HTML+ RSC Payload
- 仅适用于静态页面
- 构建时生成或 ISR 按需生成
"] end subgraph Server ["⚙️ 服务器层"] DataCache["【2. Data Cache(数据缓存)】
- 持久化缓存 fetch 结果
- 跨请求、跨用户共享
- 存储在 .next/cache 或外部存储
"] RequestMemo["【1. Request Memoization(请求记忆)】
- 单次请求内的去重
- 同一组件树中的重复 fetch 只执行一次
- 请求结束后立即清除"] end DataSource["💾 数据源层/API
-数据库 (PostgreSQL, MongoDB, etc.)
- 第三方 API
- 文件系统"] %% --- 连接关系 --- RouterCache <==>| HTTP 请求 | FullRouteCache FullRouteCache <==>| 服务端渲染 | DataCache DataCache --- RequestMemo RequestMemo <==>| 数据库/API | DataSource %% --- 样式 --- style RouterCache fill:#e1f5ff,stroke:#333,stroke-width:2px style FullRouteCache fill:#fff4e1,stroke:#333,stroke-width:2px style DataCache fill:#e8f5e9,stroke:#333,stroke-width:2px style RequestMemo fill:#e8f5e9,stroke:#333,stroke-width:2px style DataSource fill:#f3e5f5,stroke:#333,stroke-width:2px
2. 四层缓存对比表
| 缓存层 | 作用域 | 持久性 | 共享范围 | 默认行为 | 可控性 |
|---|---|---|---|---|---|
| Request Memoization | 单次请求 | 瞬时 | 同请求内组件 | 自动启用 | 不可控 |
| Data Cache | 跨请求 | 持久化 | 所有用户/请求 | 静态路由缓存 | cache/revalidate |
| Full Route Cache | 完整页面 | 持久化 | 所有用户 | 静态页面缓存 | dynamic/revalidate |
| Router Cache | 客户端 | 会话期 | 当前用户 | 5分钟TTL | router.refresh() |
三、第一层:请求记忆(Request Memoization)
1. 核心概念
请求记忆 是 Next.js 最底层的缓存机制,它在单个请求的生命周期内 对相同的 fetch 调用进行去重。
typescript
// app/products/page.tsx
async function getProducts() {
console.log('🔍 Fetching products...');
const res = await fetch('https://api.example.com/products');
return res.json();
}
async function getProductCount() {
console.log('🔍 Fetching product count...');
const res = await fetch('https://api.example.com/products?count=true');
return res.json();
}
export default async function ProductsPage() {
// ✅ 这两个 fetch 都会实际执行(URL 不同)
const products = await getProducts();
const count = await getProductCount();
return <div>...</div>;
}
typescript
// app/products/[id]/page.tsx
async function getProduct(id: string) {
console.log(`🔍 Fetching product ${id}...`);
const res = await fetch(`https://api.example.com/products/${id}`);
return res.json();
}
// 父组件
export default async function ProductDetail({ params }: { params: { id: string } }) {
// 第一次调用:实际执行 fetch
const product = await getProduct(params.id);
// 子组件中也调用同样的 URL
return (
<div>
<ProductHeader product={product} />
<ProductReviews productId={params.id} />
</div>
);
}
// 子组件
async function ProductReviews({ productId }: { productId: string }) {
// ❌ 不会再次 fetch!直接从请求记忆中读取
// 但注意:这里 URL 不同,所以实际上会再次 fetch
const reviews = await fetch(`https://api.example.com/reviews?productId=${productId}`);
return <div>...</div>;
}
2. 关键特性
- 基于 URL去重:只有完全相同的 URL(含参数) 才会命中记忆
- 自动启用:无需任何配置,Next.js 自动处理
- 请求级别:请求结束后立即清除,不占用持久化存储
- 仅针对 fetch:不适用于其他数据获取方式(如直接数据库查询)
3. 注意事项
⚠️ 请求记忆的局限性:
- 只在同一个组件树的请求中生效
- 不同的并行路由(Parallel Routes)可能不会共享记忆
- 不包含在拦截路由(Intercepting Routes)之间
四、第二层:数据缓存(Data Cache)
1. 核心概念
数据缓存 是 Next.js 最强大的缓存层,它会在服务器磁盘或外部存储 中持久化保存 fetch 的结果,跨越多个请求和用户。
2. 配置方式
(1) 通过 fetch 选项配置
typescript
// 方式一:启用缓存时间(静态渲染)
fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 缓存 1 小时
});
// 方式二:禁用缓存(默认)
fetch('https://api.example.com/data', {
cache: 'no-store' // 每次请求都重新获取
});
// 方式三:强制缓存(取决于路由类型)
fetch('https://api.example.com/data');
// - 静态路由:永久缓存
// - 动态路由:不缓存
(2) 通过路由段配置(Route Segment Config)
typescript
// app/products/page.tsx
// 整个路由设置为动态,禁用数据缓存
export const dynamic = 'force-dynamic';
// 或者强制静态
export const dynamic = 'force-static';
// 设置默认重新验证时间
export const revalidate = 3600; // 所有 fetch 默认缓存 1 小时
(3)缓存键(Cache Key)
Next.js 使用以下信息生成缓存键:
- 完整的 URL(包括查询参数)
- HTTP 方法(GET、POST 等)
- Headers (如果使用了
headers选项) - Body(对于 POST 请求)
typescript
// 这两个 fetch 会有不同的缓存键
fetch('https://api.example.com/products?page=1');
fetch('https://api.example.com/products?page=2');
// 这两个也会有不同的缓存键(即使 URL 相同)
fetch('https://api.example.com/products', { method: 'GET' });
fetch('https://api.example.com/products', { method: 'POST' });
4. 实际示例:博客文章列表
typescript
// app/blog/page.tsx
interface Post {
id: string;
title: string;
content: string;
createdAt: string;
}
async function getPosts(page: number = 1): Promise<Post[]> {
const res = await fetch(
`https://api.example.com/posts?page=${page}&limit=10`,
{
next: {
revalidate: 600, // 缓存 10 分钟
tags: ['posts'] // 添加标签用于按需重新验证
}
}
);
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
export default async function BlogPage({
searchParams
}: {
searchParams: { page?: string }
}) {
const page = Number(searchParams.page) || 1;
const posts = await getPosts(page);
return (
<div>
<h1>Blog Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.content.substring(0, 100)}...</p>
</li>
))}
</ul>
</div>
);
}
5. 按需重新验证(On-Demand Revalidation)
这是 Next.js 14+ 最强大的特性之一,允许你手动清除特定缓存。
typescript
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag, revalidatePath } from 'next/cache';
export async function POST(request: NextRequest) {
const { tag, path } = await request.json();
try {
if (tag) {
// 重新验证所有带有该标签的缓存
revalidateTag(tag);
console.log(`✅ Revalidated tag: ${tag}`);
}
if (path) {
// 重新验证特定路径
revalidatePath(path);
console.log(`✅ Revalidated path: ${path}`);
}
return NextResponse.json({
success: true,
message: 'Cache revalidated'
});
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to revalidate' },
{ status: 500 }
);
}
}
使用场景:CMS 内容更新
typescript
// 当 CMS 发布新文章时,调用此 webhook
// POST /api/revalidate
// Body: { "tag": "posts" }
// 或者重新验证特定文章详情页
// POST /api/revalidate
// Body: { "path": "/blog/my-new-post" }
6. 缓存存储位置
bash
.next/
└── cache/
├── fetch-cache/ # Data Cache 存储
│ ├── 0a1b2c3d... # 基于缓存键的哈希文件
│ └── 4e5f6g7h...
└── router-cache/ # Router Cache(开发模式)
生产环境建议:
- 使用外部缓存(Redis、Memcached)以获得更好的性能和可扩展性
- 配置 CDN 缓存以减轻服务器负载
五、第三层:全路由缓存(Full Route Cache)
1. 核心概念
全路由缓存 缓存的是整个页面的渲染结果(HTML + RSC Payload),而不仅仅是数据。这是 Next.js 静态生成的核心机制。
2. 静态 vs 动态路由
typescript
// ✅ 静态路由(会使用全路由缓存)
// app/about/page.tsx
export default function AboutPage() {
return <h1>About Us</h1>;
}
// 构建时生成 HTML,部署后直接 serving 静态文件
// ✅ 静态路由(带数据)
// app/products/page.tsx
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }
});
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return <div>{/* 渲染产品列表 */}</div>;
}
// 构建时或首次访问时生成 HTML,缓存 1 小时
// ❌ 动态路由(不使用全路由缓存)
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const user = await getCurrentUser(); // 依赖请求上下文
return <div>Welcome, {user.name}</div>;
}
// 每次请求都重新渲染
3. 增量静态再生成(ISR)
ISR 允许你在部署后按需重新生成静态页面。
typescript
// app/blog/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch(
`https://api.example.com/posts/${slug}`,
{
next: {
revalidate: 60, // 每 60 秒可以重新验证
tags: [`post-${slug}`]
}
}
);
if (!res.ok) {
return null;
}
return res.json();
}
// 静态生成:在构建时为已知 slug 生成页面
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json());
return posts.map((post: any) => ({
slug: post.slug
}));
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
工作流程:
- 构建时 :为
generateStaticParams返回的 slug 生成静态页面 - 首次访问未知 slug:实时生成页面并缓存
- 60 秒后再次访问 :
- 返回缓存的页面(快速)
- 后台重新验证数据
- 如果数据变化,下次访问返回新页面
4. 缓存失效
typescript
// 重新验证特定路径
import { revalidatePath } from 'next/cache';
export async function POST() {
// 重新验证所有产品页面
revalidatePath('/products');
// 重新验证特定产品
revalidatePath('/products/123');
// 重新验证整个博客部分
revalidatePath('/blog', 'layout');
return Response.json({ success: true });
}
六、第四层:路由器缓存(Router Cache)
1. 核心概念
路由器缓存 是客户端缓存,存储在用户的浏览器中,用于加速页面导航。
2. 工作机制
typescript
'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
export default function Navigation() {
const router = useRouter();
const handleNavigate = () => {
// 方式 1:使用 Link(自动利用路由器缓存)
// <Link href="/products">Products</Link>
// 方式 2:编程式导航
router.push('/products');
// 方式 3:强制刷新(绕过缓存)
router.refresh();
};
return <button onClick={handleNavigate}>Go to Products</button>;
}
3. 缓存行为
| 场景 | 缓存行为 |
|---|---|
| 首次访问页面 | 从服务器获取完整 RSC Payload |
| 再次访问(5分钟内) | 从路由器缓存读取(即时显示) |
| 静态页面 | 缓存永不过期(除非手动清除) |
| 动态页面 | 5分钟后后台重新验证 |
调用 router.refresh() |
强制重新获取当前路由数据 |
4. 手动控制路由器缓存
typescript
'use client';
import { useRouter } from 'next/navigation';
export default function AdminPanel() {
const router = useRouter();
const handleUpdate = async () => {
// 执行某些操作...
await updateData();
// 刷新当前路由的数据
router.refresh();
};
return <button onClick={handleUpdate}>Update & Refresh</button>;
}
6. 预取(Prefetching)
Next.js 自动预取视口中的链接,提升导航速度。
typescript
// 自动预取(默认行为)
<Link href="/products">Products</Link>
// 禁用预取
<Link href="/products" prefetch={false}>Products</Link>
// 强制预取
<Link href="/products" prefetch={true}>Products</Link>
预取规则:
- 视口中的
<Link>会自动预取 - 预取的数据存储在路由器缓存中
- 预取的是完整页面的 RSC Payload,而不仅是 HTML
七、缓存失效策略详解
1. 基于时间的失效(Time-Based Revalidation)
typescript
// 固定时间重新验证
fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 1 小时
});
// 路由级别配置
export const revalidate = 600; // 所有数据默认缓存 10 分钟
2. 基于标签的失效(Tag-Based Revalidation)
typescript
// 标记缓存
fetch('https://api.example.com/products', {
next: { tags: ['products'] }
});
fetch('https://api.example.com/categories', {
next: { tags: ['categories'] }
});
// 按需重新验证
import { revalidateTag } from 'next/cache';
revalidateTag('products'); // 清除所有 products 标签的缓存
3. 基于路径的失效(Path-Based Revalidation)
typescript
import { revalidatePath } from 'next/cache';
// 重新验证特定页面
revalidatePath('/products/123');
// 重新验证整个目录
revalidatePath('/products');
// 重新验证布局
revalidatePath('/dashboard', 'layout');
4. 手动失效(Manual Revalidation)
typescript
// Server Action 中
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updateProduct(formData: FormData) {
// 更新数据库...
await db.product.update({ /* ... */ });
// 清除相关缓存
revalidatePath('/products');
revalidateTag('products');
return { success: true };
}
5. 自动失效场景
| 场景 | 自动失效的缓存 |
|---|---|
| POST/PUT/DELETE Server Action | 当前路由的 Data Cache |
router.refresh() |
当前路由的 Router Cache |
| 部署新版本 | 所有 Full Route Cache |
修改 next.config.js |
所有缓存 |
八、Server Actions 与缓存交互
1. 自动重新验证
当你使用 Server Actions 修改数据时,Next.js 会自动重新验证相关缓存。
typescript
// app/products/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createProduct(formData: FormData) {
const name = formData.get('name');
const price = formData.get('price');
// 创建产品
await db.product.create({
data: { name, price }
});
// ✅ 自动重新验证当前路由
// 如果你在这个 action 所在的页面调用它
// Next.js 会自动调用 revalidatePath(currentPath)
return { success: true };
}
2. 显式控制
typescript
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updateProduct(id: string, formData: FormData) {
// 更新产品
await db.product.update({
where: { id },
data: { /* ... */ }
});
// 精确控制缓存失效
revalidatePath(`/products/${id}`);
revalidatePath('/products');
revalidateTag('products');
return { success: true };
}
3. 乐观更新(Optimistic Updates)
typescript
'use client';
import { useState, useOptimistic } from 'react';
import { updateProduct } from './actions';
export default function ProductList({ initialProducts }: { initialProducts: Product[] }) {
const [products, setProducts] = useState(initialProducts);
// 乐观更新
const [optimisticProducts, addOptimisticUpdate] = useOptimistic(
products,
(state, updatedProduct: Product) => {
return state.map(p => p.id === updatedProduct.id ? updatedProduct : p);
}
);
const handleUpdate = async (product: Product) => {
// 立即更新 UI
addOptimisticUpdate(product);
// 后台执行实际更新
const formData = new FormData();
formData.append('name', product.name);
await updateProduct(product.id, formData);
};
return (
<ul>
{optimisticProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
八、高级缓存场景与最佳实践
场景 1:个性化内容的缓存策略
typescript
// ❌ 错误:缓存了个性化数据
export default async function DashboardPage() {
const user = await getCurrentUser(); // 每个用户不同
const stats = await getUserStats(user.id);
return <div>Welcome, {user.name}</div>;
}
// ✅ 正确:强制动态渲染
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const user = await getCurrentUser();
const stats = await getUserStats(user.id);
return <div>Welcome, {user.name}</div>;
}
场景 2:混合缓存策略
typescript
// app/products/[id]/page.tsx
// 产品信息:缓存 1 小时
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 3600, tags: [`product-${id}`] }
});
return res.json();
}
// 库存信息:不缓存(实时)
async function getStock(id: string) {
const res = await fetch(`https://api.example.com/stock/${id}`, {
cache: 'no-store'
});
return res.json();
}
// 评论:缓存 10 分钟
async function getReviews(id: string) {
const res = await fetch(`https://api.example.com/reviews?productId=${id}`, {
next: { revalidate: 600, tags: [`reviews-${id}`] }
});
return res.json();
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const [product, stock, reviews] = await Promise.all([
getProduct(params.id),
getStock(params.id),
getReviews(params.id)
]);
return (
<div>
<h1>{product.name}</h1>
<p>Stock: {stock.quantity}</p>
<Reviews reviews={reviews} />
</div>
);
}
场景 3:缓存预热(Cache Warming)
typescript
// app/api/warm-cache/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
// 预热热门产品的缓存
const popularProducts = await getPopularProductIds();
await Promise.all(
popularProducts.map(async (id) => {
// 触发数据获取并缓存
await fetch(`https://your-app.com/products/${id}`);
})
);
return NextResponse.json({ success: true });
}
// 定时任务:每小时执行一次
// 可以使用 Vercel Cron Jobs 或其他调度服务
场景 4:多级缓存架构
typescript
// lib/cache.ts
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
// 自定义缓存层
export async function cachedFetch<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = 3600
): Promise<T> {
// 1. 尝试从 Redis 读取
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
// 2. 执行实际请求
const data = await fetcher();
// 3. 存入 Redis
await redis.setex(key, ttl, JSON.stringify(data));
return data;
}
// 使用示例
async function getProducts() {
return cachedFetch(
'products:all',
async () => {
const res = await fetch('https://api.example.com/products');
return res.json();
},
1800 // 30 分钟
);
}
最佳实践清单
✅ 应该做的:
- 明确标注缓存策略 :始终指定
revalidate或cache: 'no-store' - 使用标签管理缓存:便于批量失效
- 分层缓存:热数据用内存,冷数据用磁盘
- 监控缓存命中率:了解缓存效果
- 设置合理的 TTL:根据数据变化频率调整
- 测试缓存失效:确保更新能及时反映
❌ 不应该做的:
- 不要缓存个性化数据:用户特定的内容应设为动态
- 不要过度缓存:频繁变化的数据缓存意义不大
- 不要忘记清理缓存:避免内存泄漏
- 不要依赖默认行为:显式声明意图
- 不要在客户端缓存敏感数据:注意安全
九、完整实战案例:电商产品页面
让我们构建一个完整的电商产品页面,展示各种缓存策略的综合应用。
1. 项目结构
bash
app/
├── products/
│ ├── [id]/
│ │ ├── page.tsx # 产品详情页
│ │ └── loading.tsx # 加载状态
│ ├── page.tsx # 产品列表页
│ └── actions.ts # Server Actions
├── api/
│ └── revalidate/
│ └── route.ts # 缓存失效 API
├── layout.tsx
└── page.tsx
1. 产品列表页
typescript
// app/products/page.tsx
import Link from 'next/link';
import { Suspense } from 'react';
interface Product {
id: string;
name: string;
price: number;
image: string;
category: string;
}
// 缓存产品列表 5 分钟
async function getProducts(category?: string): Promise<Product[]> {
const url = category
? `https://api.example.com/products?category=${category}`
: 'https://api.example.com/products';
const res = await fetch(url, {
next: {
revalidate: 300,
tags: ['products']
}
});
if (!res.ok) {
throw new Error('Failed to fetch products');
}
return res.json();
}
// 缓存分类列表 1 小时
async function getCategories(): Promise<string[]> {
const res = await fetch('https://api.example.com/categories', {
next: {
revalidate: 3600,
tags: ['categories']
}
});
return res.json();
}
// 产品卡片组件(客户端组件)
'use client';
import { useState } from 'react';
function ProductCard({ product }: { product: Product }) {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className="border rounded-lg p-4 transition-shadow hover:shadow-lg"
>
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover rounded"
/>
<h3 className="mt-2 font-semibold">{product.name}</h3>
<p className="text-blue-600">${product.price}</p>
<Link
href={`/products/${product.id}`}
className="mt-2 inline-block text-sm text-blue-500 hover:underline"
>
View Details
</Link>
</div>
);
}
// 产品网格(服务端组件)
async function ProductGrid({ category }: { category?: string }) {
const products = await getProducts(category);
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// 主页面
export default async function ProductsPage({
searchParams
}: {
searchParams: { category?: string }
}) {
const categories = await getCategories();
const selectedCategory = searchParams.category;
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Products</h1>
{/* 分类过滤器 */}
<div className="mb-6 flex gap-2 flex-wrap">
<Link
href="/products"
className={`px-4 py-2 rounded ${
!selectedCategory ? 'bg-blue-500 text-white' : 'bg-gray-200'
}`}
>
All
</Link>
{categories.map(category => (
<Link
key={category}
href={`/products?category=${category}`}
className={`px-4 py-2 rounded ${
selectedCategory === category
? 'bg-blue-500 text-white'
: 'bg-gray-200'
}`}
>
{category}
</Link>
))}
</div>
{/* 产品列表(带 Suspense) */}
<Suspense fallback={<div>Loading products...</div>}>
<ProductGrid category={selectedCategory} />
</Suspense>
</div>
);
}
// 生成静态参数(用于 ISR)
export async function generateStaticParams() {
const categories = await getCategories();
return categories.map(category => ({
category
}));
}
// 每 10 分钟重新验证
export const revalidate = 600;
2. 产品详情页
typescript
// app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
import { Suspense } from 'react';
interface Product {
id: string;
name: string;
description: string;
price: number;
images: string[];
stock: number;
}
interface Review {
id: string;
user: string;
rating: number;
comment: string;
createdAt: string;
}
// 产品详情:缓存 1 小时
async function getProduct(id: string): Promise<Product> {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: {
revalidate: 3600,
tags: [`product-${id}`]
}
});
if (!res.ok) {
if (res.status === 404) {
notFound();
}
throw new Error('Failed to fetch product');
}
return res.json();
}
// 库存信息:不缓存(实时)
async function getStock(id: string): Promise<number> {
const res = await fetch(`https://api.example.com/stock/${id}`, {
cache: 'no-store'
});
const data = await res.json();
return data.quantity;
}
// 评论:缓存 10 分钟
async function getReviews(productId: string): Promise<Review[]> {
const res = await fetch(
`https://api.example.com/reviews?productId=${productId}`,
{
next: {
revalidate: 600,
tags: [`reviews-${productId}`]
}
}
);
return res.json();
}
// 相关产品:缓存 30 分钟
async function getRelatedProducts(productId: string): Promise<Product[]> {
const res = await fetch(
`https://api.example.com/products/related/${productId}`,
{
next: {
revalidate: 1800,
tags: ['products']
}
}
);
return res.json();
}
// 添加到购物车(Server Action)
'use server';
import { revalidateTag } from 'next/cache';
export async function addToCart(productId: string, quantity: number) {
// 模拟添加到购物车
await new Promise(resolve => setTimeout(resolve, 500));
// 重新验证库存
revalidateTag(`product-${productId}`);
return { success: true };
}
// 产品图片画廊(客户端组件)
'use client';
import { useState } from 'react';
function ImageGallery({ images }: { images: string[] }) {
const [currentIndex, setCurrentIndex] = useState(0);
return (
<div>
<img
src={images[currentIndex]}
alt="Product"
className="w-full h-96 object-cover rounded-lg"
/>
<div className="flex gap-2 mt-4">
{images.map((img, idx) => (
<button
key={idx}
onClick={() => setCurrentIndex(idx)}
className={`border-2 rounded ${
idx === currentIndex ? 'border-blue-500' : 'border-transparent'
}`}
>
<img src={img} alt="" className="w-20 h-20 object-cover" />
</button>
))}
</div>
</div>
);
}
// 评论列表(客户端组件)
'use client';
import { useState } from 'react';
function ReviewsList({ reviews }: { reviews: Review[] }) {
const [sortBy, setSortBy] = useState<'newest' | 'highest'>('newest');
const sortedReviews = [...reviews].sort((a, b) => {
if (sortBy === 'newest') {
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
}
return b.rating - a.rating;
});
return (
<div>
<div className="mb-4">
<label className="mr-2">Sort by:</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="border rounded px-2 py-1"
>
<option value="newest">Newest</option>
<option value="highest">Highest Rating</option>
</select>
</div>
<div className="space-y-4">
{sortedReviews.map(review => (
<div key={review.id} className="border-b pb-4">
<div className="flex items-center gap-2">
<span className="font-semibold">{review.user}</span>
<span className="text-yellow-500">
{'★'.repeat(review.rating)}
</span>
</div>
<p className="mt-2">{review.comment}</p>
<p className="text-sm text-gray-500 mt-1">
{new Date(review.createdAt).toLocaleDateString()}
</p>
</div>
))}
</div>
</div>
);
}
// 主页面
export default async function ProductDetailPage({
params
}: {
params: { id: string }
}) {
// 并行获取数据
const [product, stock, reviews, relatedProducts] = await Promise.all([
getProduct(params.id),
getStock(params.id),
getReviews(params.id),
getRelatedProducts(params.id)
]);
return (
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* 左侧:图片 */}
<ImageGallery images={product.images} />
{/* 右侧:产品信息 */}
<div>
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-2xl text-blue-600 mt-4">${product.price}</p>
<div className="mt-4">
<span className={`inline-block px-3 py-1 rounded ${
stock > 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{stock > 0 ? `In Stock (${stock})` : 'Out of Stock'}
</span>
</div>
<p className="mt-6 text-gray-700">{product.description}</p>
{/* 添加到购物车表单 */}
<form action={async (formData) => {
'use server';
await addToCart(params.id, 1);
}}>
<button
type="submit"
disabled={stock === 0}
className="mt-6 w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600 disabled:bg-gray-400"
>
Add to Cart
</button>
</form>
</div>
</div>
{/* 评论部分 */}
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">Reviews</h2>
<Suspense fallback={<div>Loading reviews...</div>}>
<ReviewsList reviews={reviews} />
</Suspense>
</div>
{/* 相关产品 */}
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">Related Products</h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{relatedProducts.map(product => (
<Link
key={product.id}
href={`/products/${product.id}`}
className="border rounded-lg p-4 hover:shadow-lg transition"
>
<img
src={product.images[0]}
alt={product.name}
className="w-full h-32 object-cover rounded"
/>
<h3 className="mt-2 font-semibold">{product.name}</h3>
<p className="text-blue-600">${product.price}</p>
</Link>
))}
</div>
</div>
</div>
);
}
// 生成元数据
export async function generateMetadata({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id);
return {
title: `${product.name} | My Store`,
description: product.description,
openGraph: {
images: [product.images[0]]
}
};
}
// 生成静态参数
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(res => res.json());
return products.slice(0, 100).map((product: any) => ({
id: product.id
}));
}
3. 缓存失效 API
typescript
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag, revalidatePath } from 'next/cache';
export async function POST(request: NextRequest) {
// 验证 webhook secret(生产环境必需)
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.WEBHOOK_SECRET) {
return NextResponse.json(
{ error: 'Invalid secret' },
{ status: 401 }
);
}
const body = await request.json();
const { type, id, tag, path } = body;
try {
switch (type) {
case 'product_updated':
// 产品更新:重新验证相关产品缓存
revalidateTag(`product-${id}`);
revalidatePath(`/products/${id}`);
revalidateTag('products');
break;
case 'product_created':
// 新产品:重新验证产品列表
revalidateTag('products');
revalidatePath('/products');
break;
case 'review_added':
// 新评论:重新验证评论缓存
revalidateTag(`reviews-${id}`);
break;
case 'stock_updated':
// 库存更新:重新验证产品缓存
revalidateTag(`product-${id}`);
break;
default:
if (tag) {
revalidateTag(tag);
}
if (path) {
revalidatePath(path);
}
}
return NextResponse.json({
success: true,
message: 'Cache revalidated successfully'
});
} catch (error) {
console.error('Revalidation error:', error);
return NextResponse.json(
{ error: 'Failed to revalidate cache' },
{ status: 500 }
);
}
}
4. 管理后台:手动触发重新验证
typescript
// app/admin/cache/page.tsx
'use client';
import { useState } from 'react';
export default function CacheManagement() {
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const revalidate = async (type: string, id?: string) => {
setLoading(true);
setMessage('');
try {
const res = await fetch('/api/revalidate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-webhook-secret': process.env.NEXT_PUBLIC_WEBHOOK_SECRET || ''
},
body: JSON.stringify({ type, id })
});
const data = await res.json();
if (data.success) {
setMessage('✅ Cache revalidated successfully');
} else {
setMessage('❌ Failed to revalidate cache');
}
} catch (error) {
setMessage('❌ Error: ' + error);
} finally {
setLoading(false);
}
};
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Cache Management</h1>
<div className="space-y-4">
<div className="border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">Products</h2>
<div className="flex gap-2">
<button
onClick={() => revalidate('product_created')}
disabled={loading}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:bg-gray-400"
>
Revalidate All Products
</button>
<button
onClick={() => {
const id = prompt('Enter product ID:');
if (id) revalidate('product_updated', id);
}}
disabled={loading}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 disabled:bg-gray-400"
>
Revalidate Specific Product
</button>
</div>
</div>
<div className="border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">Reviews</h2>
<button
onClick={() => {
const id = prompt('Enter product ID:');
if (id) revalidate('review_added', id);
}}
disabled={loading}
className="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600 disabled:bg-gray-400"
>
Revalidate Reviews
</button>
</div>
</div>
{message && (
<div className="mt-6 p-4 bg-gray-100 rounded">
{message}
</div>
)}
</div>
);
}
十、常见陷阱与调试技巧
1. 常见陷阱
陷阱 1:忘记禁用个性化数据的缓存
typescript
// ❌ 错误:每个用户看到的都是第一个用户的缓存
export default async function ProfilePage() {
const user = await getCurrentUser();
return <div>{user.name}</div>;
}
// ✅ 正确:强制动态渲染
export const dynamic = 'force-dynamic';
export default async function ProfilePage() {
const user = await getCurrentUser();
return <div>{user.name}</div>;
}
陷阱 2:在客户端组件中使用 revalidateTag
typescript
// ❌ 错误:revalidateTag 只能在服务端使用
'use client';
import { revalidateTag } from 'next/cache'; // 🔴 这会报错!
// ✅ 正确:在 Server Action 中使用
'use server';
import { revalidateTag } from 'next/cache';
export async function updateData() {
revalidateTag('my-tag');
}
陷阱 3:期望 router.refresh() 清除所有缓存
typescript
// ❌ 错误理解:认为这会清除 Data Cache
router.refresh(); // 只清除 Router Cache
// ✅ 正确:需要配合 Server Action
'use server';
import { revalidatePath } from 'next/cache';
export async function updateAndRefresh() {
// 更新数据...
revalidatePath('/current-page');
// 然后在客户端调用 router.refresh()
}
陷阱 4:在开发环境中混淆缓存行为
开发环境中,Next.js 的缓存行为与生产环境不同:
- Data Cache:默认禁用(每次请求都重新获取)
- Router Cache:更短的 TTL
解决方案:使用环境变量区分配置
typescript
const isDev = process.env.NODE_ENV === 'development';
fetch('https://api.example.com/data', {
next: {
revalidate: isDev ? 0 : 3600 // 开发环境不缓存
}
});
2. 调试技巧
(1) 启用缓存日志
typescript
// next.config.js
module.exports = {
logging: {
fetches: {
fullUrl: true,
},
},
};
(2) 检查缓存状态
typescript
// 在响应头中添加缓存信息
export async function GET() {
const data = await fetchData();
return new Response(JSON.stringify(data), {
headers: {
'X-Cache-Status': 'HIT', // 或 MISS
'X-Cache-Tags': 'products,categories'
}
});
}
(3)使用浏览器开发者工具
- Network 面板:查看请求是否从缓存读取
- Application 面板:检查 Service Worker 缓存
- Performance 面板:分析页面加载性能
(5) 监控工具
typescript
// lib/monitoring.ts
export function trackCacheHit(type: 'router' | 'data' | 'route', hit: boolean) {
// 发送到监控系统
analytics.track('cache_event', {
type,
hit,
timestamp: Date.now()
});
}
十一、性能监控与优化
1. 关键指标
(1)缓存命中率(Cache Hit Rate)
bash
命中率 = 缓存命中次数 / 总请求次数
目标:> 80%
(2)平均响应时间
- 缓存命中:< 100ms
- 缓存未命中:500ms - 2s
(3)缓存大小
- 监控
.next/cache目录大小 - 设置缓存上限,定期清理
2. 监控实现
typescript
// middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const startTime = Date.now();
const response = NextResponse.next();
// 记录响应时间
response.headers.set('X-Response-Time', `${Date.now() - startTime}ms`);
return response;
}
3. 优化建议
- 使用 CDN:将静态资源分发到全球边缘节点
- 实施分级缓存 :
- L1:内存缓存(Redis)
- L2:磁盘缓存(.next/cache)
- L3:CDN 缓存
- 预热热门页面:在低峰期预先生成缓存
- 智能失效:只清除必要的缓存,避免缓存雪崩
十二、总结与展望
1. 核心要点回顾
(1)四层缓存架构:
- Request Memoization:请求内去重
- Data Cache:持久化数据缓存
- Full Route Cache:完整页面缓存
- Router Cache:客户端路由缓存
(2)缓存控制:
- 时间驱动:
revalidate - 标签驱动:
revalidateTag - 路径驱动:
revalidatePath - 手动驱动:
router.refresh()
(3)最佳实践:
- 明确声明缓存策略
- 使用标签管理缓存
- 避免缓存个性化数据
- 监控缓存性能
2. 未来趋势
- 边缘缓存:更多计算推向边缘节点
- AI 驱动的缓存预测:智能预判用户行为
- 分布式缓存:跨区域缓存同步
- WebAssembly 缓存:更高效的序列化
下一章将深入性能优化领域------了解如何测量、分析和提升 Next.js 应用的性能指标。