Next.js从入门到实战保姆级教程(第十三章):从原理到实践深度剖析缓存策略

本系列文章将围绕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) 实现了四层缓存架构,每一层都有不同的职责和生命周期:

graph TB %% --- 节点定义 --- subgraph Client ["🌐 浏览器层"] %% 在短行后面加了全角空格,让它看起来和长行一样宽 RouterCache["【4. Router Cache(客户端路由缓存)】
- 存储已访问页面的 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. 关键特性

  1. 基于 URL去重:只有完全相同的 URL(含参数) 才会命中记忆
  2. 自动启用:无需任何配置,Next.js 自动处理
  3. 请求级别:请求结束后立即清除,不占用持久化存储
  4. 仅针对 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>
  );
}

工作流程

  1. 构建时 :为 generateStaticParams 返回的 slug 生成静态页面
  2. 首次访问未知 slug:实时生成页面并缓存
  3. 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 分钟
  );
}

最佳实践清单

应该做的

  1. 明确标注缓存策略 :始终指定 revalidatecache: 'no-store'
  2. 使用标签管理缓存:便于批量失效
  3. 分层缓存:热数据用内存,冷数据用磁盘
  4. 监控缓存命中率:了解缓存效果
  5. 设置合理的 TTL:根据数据变化频率调整
  6. 测试缓存失效:确保更新能及时反映

不应该做的

  1. 不要缓存个性化数据:用户特定的内容应设为动态
  2. 不要过度缓存:频繁变化的数据缓存意义不大
  3. 不要忘记清理缓存:避免内存泄漏
  4. 不要依赖默认行为:显式声明意图
  5. 不要在客户端缓存敏感数据:注意安全

九、完整实战案例:电商产品页面

让我们构建一个完整的电商产品页面,展示各种缓存策略的综合应用。

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. 优化建议

  1. 使用 CDN:将静态资源分发到全球边缘节点
  2. 实施分级缓存
    • L1:内存缓存(Redis)
    • L2:磁盘缓存(.next/cache)
    • L3:CDN 缓存
  3. 预热热门页面:在低峰期预先生成缓存
  4. 智能失效:只清除必要的缓存,避免缓存雪崩

十二、总结与展望

1. 核心要点回顾

(1)四层缓存架构

  • Request Memoization:请求内去重
  • Data Cache:持久化数据缓存
  • Full Route Cache:完整页面缓存
  • Router Cache:客户端路由缓存

(2)缓存控制

  • 时间驱动:revalidate
  • 标签驱动:revalidateTag
  • 路径驱动:revalidatePath
  • 手动驱动:router.refresh()

(3)最佳实践

  • 明确声明缓存策略
  • 使用标签管理缓存
  • 避免缓存个性化数据
  • 监控缓存性能

2. 未来趋势

  • 边缘缓存:更多计算推向边缘节点
  • AI 驱动的缓存预测:智能预判用户行为
  • 分布式缓存:跨区域缓存同步
  • WebAssembly 缓存:更高效的序列化

下一章将深入性能优化领域------了解如何测量、分析和提升 Next.js 应用的性能指标。

相关推荐
ejinxian1 小时前
Rust的GUI方案中,Slint、Azul、egui、iced、Druid、Tauri
前端·javascript·vue.js
威迪斯特1 小时前
Cobra框架:Go语言命令行开发的现代化利器
开发语言·前端·后端·golang·cobra·交互模型·命令行框架
Python私教2 小时前
ShadcnVueAdmin 的国际化是怎么实现的
前端·javascript·vue.js
㳺三才人子2 小时前
容器內的 H2 控制台
开发语言·前端·javascript
谷子熟了2 小时前
电商智能客服系统本地搭建
经验分享·docker·typescript·ai编程·llama
光影少年2 小时前
vite+rust生态链工具链
开发语言·前端·后端·rust·前端框架
skywalk81632 小时前
当前有什么流行的lisp的web框架吗?
开发语言·前端·lisp
IT_陈寒2 小时前
为什么我的JavaScript变量老是不听使唤?
前端·人工智能·后端
HookJames2 小时前
设计Section 06 · Component Sourcing & BOM Risk Control
前端