跳到主要内容

缓存机制

缓存是 Next.js 性能优化的核心机制。理解 Next.js 的缓存策略对于构建高性能应用至关重要。本章将详细介绍 Next.js 的多层缓存系统及其使用方法。

缓存概览

Next.js 提供了四种相互独立的缓存机制:

缓存类型作用位置失效方式
请求记忆化在渲染过程中去重 fetch 请求服务端内存每次请求后清除
数据缓存在请求之间持久化 fetch 结果服务端文件系统手动失效或定时重新验证
全路由缓存缓存路由的渲染结果服务端文件系统重新验证或重新部署
路由缓存缓存路由段客户端内存基于时间或用户操作

理解这些缓存的工作原理和交互方式,能帮助你做出正确的架构决策。

请求记忆化(Request Memoization)

工作原理

请求记忆化是一种在同一次渲染过程中自动去重相同 fetch 请求的机制。

// components/user-info.tsx
async function getUser() {
const res = await fetch("https://api.example.com/user");
return res.json();
}

export default async function UserInfo() {
const user = await getUser();
return <div>{user.name}</div>;
}

// components/user-avatar.tsx
async function getUser() {
const res = await fetch("https://api.example.com/user"); // 自动去重
return res.json();
}

export default async function UserAvatar() {
const user = await getUser();
return <img src={user.avatar} />;
}

当这两个组件在同一页面中渲染时,fetch("https://api.example.com/user") 只会执行一次,第二个组件会复用第一个请求的结果。

工作流程

适用范围

请求记忆化只适用于:

  • fetch 请求(GET 方法)
  • React 组件树中的请求
  • 同一次渲染过程

不适用于:

  • POST、PUT、DELETE 等非 GET 请求
  • fetch 之外的 API(如数据库查询)
  • 不同页面或不同请求

对非 fetch 数据使用记忆化

对于数据库查询等操作,可以使用 React 的 cache 函数:

// lib/user.ts
import { cache } from "react";
import { db } from "./db";

export const getUser = cache(async (id: string) => {
return db.user.findUnique({
where: { id },
});
});
// 在多个组件中使用
import { getUser } from "@/lib/user";

// 组件 A
export default async function UserProfile({ id }: { id: string }) {
const user = await getUser(id);
return <div>{user.name}</div>;
}

// 组件 B(同一次渲染中会复用结果)
export default async function UserEmail({ id }: { id: string }) {
const user = await getUser(id); // 复用组件 A 的结果
return <div>{user.email}</div>;
}

数据缓存(Data Cache)

工作原理

数据缓存将 fetch 请求的结果持久化存储在服务端文件系统中,可以跨多次请求和部署保持数据。

Next.js 15 的默认行为变化

重要变更:在 Next.js 15 中,fetch 请求默认不缓存

// Next.js 14: 默认缓存
// Next.js 15: 默认不缓存
const res = await fetch("https://api.example.com/data");

启用数据缓存

显式启用缓存:

// 强制缓存
const res = await fetch("https://api.example.com/data", {
cache: "force-cache",
});

禁用数据缓存

显式禁用缓存(每次请求都获取新数据):

const res = await fetch("https://api.example.com/data", {
cache: "no-store",
});

时间基重新验证(ISR)

设置重新验证时间间隔,实现增量静态再生:

// 每 60 秒重新验证
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});

重新验证的工作流程:

  1. 首次请求时,数据被缓存
  2. 60 秒内的后续请求使用缓存数据
  3. 60 秒后的首次请求仍返回缓存数据,同时后台触发重新验证
  4. 新数据获取成功后,缓存被更新

按需重新验证

为请求添加标签,支持精确的缓存失效:

// 设置标签
export async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
return res.json();
}

export async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { tags: ["post", `post-${id}`] },
});
return res.json();
}

在 Server Action 或 Route Handler 中触发重新验证:

"use server";

import { revalidateTag, revalidatePath } from "next/cache";

// 重新验证所有带 "posts" 标签的请求
export async function refreshPosts() {
revalidateTag("posts");
}

// 重新验证特定路径
export async function refreshPostPage() {
revalidatePath("/posts");
}

// 重新验证特定文章
export async function refreshPost(id: string) {
revalidateTag(`post-${id}`);
}

非 fetch 数据的缓存

对于数据库查询等非 fetch 操作,使用 unstable_cache

import { unstable_cache } from "next/cache";

export const getCachedUser = unstable_cache(
async (id: string) => {
return db.user.findUnique({
where: { id },
});
},
["user"], // 缓存键
{
revalidate: 3600, // 每小时重新验证
tags: ["user"], // 用于按需重新验证
}
);

use cache 指令

Next.js 15 引入了 use cache 指令,这是一种声明式的缓存方式,可以在函数、组件或文件级别启用缓存。

启用配置

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
experimental: {
useCache: true, // Next.js 15
},
// 或 Next.js 16+
// cacheComponents: true,
};

export default nextConfig;

文件级别缓存

在文件顶部添加 "use cache",所有导出的函数都会被缓存:

"use cache";

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();
}

函数级别缓存

在特定函数内部添加 "use cache"

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

// 这个函数不会被缓存
export async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`);
return res.json();
}

组件级别缓存

缓存整个组件的输出:

export async function ProductList({ category }: { category: string }) {
"use cache";
const products = await getProducts(category);

return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}

缓存生命周期配置

使用 cacheLife 函数配置缓存策略:

import { cacheLife } from "next/cache";

export async function getProducts() {
"use cache";
cacheLife("hours"); // 预设: seconds | minutes | hours | days | weeks | max

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

// 自定义配置
export async function getAnalytics() {
"use cache";
cacheLife({
stale: 3600, // 客户端缓存 1 小时
revalidate: 900, // 服务器 15 分钟验证一次
expire: 86400, // 1 天后过期
});

// ...
}

缓存标签

使用 cacheTag 为缓存添加标签:

import { cacheTag } from "next/cache";

export async function getPosts() {
"use cache";
cacheTag("posts");

const res = await fetch("https://api.example.com/posts");
return res.json();
}

按需重新验证:

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

// 重新验证(下次请求时更新)
revalidateTag("posts");

// 立即更新并重新验证
updateTag("posts");

使用限制

use cache 有以下限制:

  • 函数参数和返回值必须是可序列化的
  • 不能直接使用请求时 API(cookies()headers()
  • 不能使用不可序列化的值

如果需要在缓存函数中使用请求相关数据,应通过参数传递:

// 错误:不能直接使用 cookies
export async function getUserData() {
"use cache";
const cookieStore = await cookies(); // 错误!
// ...
}

// 正确:通过参数传递
export async function getUserData(userId: string) {
"use cache";
// userId 来自外部传入
return db.user.findUnique({ where: { id: userId } });
}

全路由缓存(Full Route Cache)

工作原理

全路由缓存将渲染后的路由存储在服务端文件系统中,用于加速后续请求。

静态路由

静态路由在构建时渲染并缓存:

// app/about/page.tsx
export default async function AboutPage() {
// 构建时获取数据
const content = await getAboutContent();
return <div>{content}</div>;
}

动态路由

使用 generateStaticParams 预渲染动态路由:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}

export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}

静态与动态的判定

以下情况会使路由变为动态:

API行为
cookies()动态渲染
headers()动态渲染
searchParams prop动态渲染
no-store fetch动态渲染
动态函数动态渲染

以下配置可以强制静态或动态:

// 强制静态
export const dynamic = "force-static";

// 强制动态
export const dynamic = "force-dynamic";

// 设置重新验证时间
export const revalidate = 3600;

路由缓存(Router Cache)

工作原理

路由缓存将路由段存储在客户端内存中,加速客户端导航。

缓存时间

场景缓存时间
预取(静态路由)5 分钟
预取(动态路由)不缓存
用户访问5 分钟

影响缓存的因素

预取行为

// 默认:进入视口时预取
<Link href="/about">关于</Link>

// 禁用预取
<Link href="/about" prefetch={false}>关于</Link>

// 预取所有静态路由
<Link href="/about" prefetch={true}>关于</Link>

页面配置

// 禁用预取
export const experimental_ppr = false;

// 设置重新验证时间
export const revalidate = 60;

失效路由缓存

用户操作

  • 点击链接导航
  • 使用 router.refresh()
  • 使用浏览器的刷新按钮

编程方式

"use client";

import { useRouter } from "next/navigation";

export default function RefreshButton() {
const router = useRouter();

return (
<button onClick={() => router.refresh()}>
刷新数据
</button>
);
}

页面级缓存配置

revalidate 选项

// 默认:静态页面
export const revalidate = false;

// 定时重新验证
export const revalidate = 3600; // 秒

// 动态渲染
export const revalidate = 0;

dynamic 选项

// 自动判断(默认)
export const dynamic = "auto";

// 强制静态
export const dynamic = "force-static";

// 强制动态
export const dynamic = "force-dynamic";

// 错误处理
export const dynamic = "error";

fetchCache 选项

控制页面内所有 fetch 的默认缓存行为:

// 默认行为
export const fetchCache = "auto";

// 使用缓存(除非显式设置 no-store)
export const fetchCache = "default-cache";

// 不使用缓存(除非显式设置 force-cache)
export const fetchCache = "default-no-store";

// 只使用缓存
export const fetchCache = "force-cache";

// 只使用动态请求
export const fetchCache = "only-no-store";

缓存最佳实践

1. 选择合适的缓存策略

场景推荐策略
静态内容(如博客文章)静态生成 + 长时间缓存
频繁更新的内容ISR + 短时间重新验证
用户个性化内容动态渲染 + 客户端获取
实时数据禁用缓存

2. 合理使用标签

// 按实体类型分组
next: { tags: ["posts"] }
next: { tags: ["users"] }

// 包含实体 ID
next: { tags: ["post", `post-${id}`] }

// 按用户分组
next: { tags: [`user-${userId}-posts`] }

3. 缓存验证

使用 Zod 等工具验证缓存数据:

import { z } from "zod";

const postSchema = z.object({
id: z.number(),
title: z.string(),
content: z.string(),
});

export async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { tags: [`post-${id}`] },
});

const data = await res.json();
return postSchema.parse(data); // 验证数据结构
}

4. 缓存预热

对于重要页面,可以在构建后预热缓存:

// 在重新验证时触发相关数据的预加载
export async function revalidatePost(id: string) {
"use server";

revalidateTag(`post-${id}`);

// 预热:立即获取新数据
await getPost(id);
}

缓存调试

开发环境行为

在开发环境中,全路由缓存默认禁用,方便调试。

查看缓存状态

使用 next build 查看静态生成的页面:

npm run build

输出会显示每个路由的渲染方式:

  • 静态
  • ƒ 动态
  • λ 服务端

调试数据缓存

添加日志追踪缓存命中:

export async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"], revalidate: 60 },
});

const cacheStatus = res.headers.get("x-nextjs-cache");
console.log("Cache status:", cacheStatus);
// HIT: 缓存命中
// MISS: 缓存未命中
// STALE: 数据过期

return res.json();
}

小结

本章我们学习了:

  1. Next.js 的四种缓存机制及其工作原理
  2. 请求记忆化:同一次渲染中的请求去重
  3. 数据缓存:跨请求的数据持久化
  4. use cache 指令:声明式缓存
  5. 全路由缓存:路由渲染结果的缓存
  6. 路由缓存:客户端导航优化
  7. 缓存配置和最佳实践

练习

  1. 创建一个博客列表页面,使用 ISR 每小时更新一次
  2. 实现一个带标签重新验证的文章详情页
  3. 使用 use cache 指令缓存数据库查询
  4. 配置不同页面的缓存策略,观察行为差异