缓存机制
缓存是 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 },
});
重新验证的工作流程:
- 首次请求时,数据被缓存
- 60 秒内的后续请求使用缓存数据
- 60 秒后的首次请求仍返回缓存数据,同时后台触发重新验证
- 新数据获取成功后,缓存被更新
按需重新验证
为请求添加标签,支持精确的缓存失效:
// 设置标签
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();
}
小结
本章我们学习了:
- Next.js 的四种缓存机制及其工作原理
- 请求记忆化:同一次渲染中的请求去重
- 数据缓存:跨请求的数据持久化
use cache指令:声明式缓存- 全路由缓存:路由渲染结果的缓存
- 路由缓存:客户端导航优化
- 缓存配置和最佳实践
练习
- 创建一个博客列表页面,使用 ISR 每小时更新一次
- 实现一个带标签重新验证的文章详情页
- 使用
use cache指令缓存数据库查询 - 配置不同页面的缓存策略,观察行为差异