Next 和 OpenNext 中的缓存
在 Vercel 之外使用 Next 时,缓存可能会很快变得棘手。有很多事情需要考虑。
通常,你会在 Next 应用前面部署一个 CDN。这个 CDN 将缓存来自你的 Next 应用的响应并提供给用户。这对性能很有好处,但当你需要使缓存失效时,这也可能成为一个问题。我们在本文档中提供了一些代码示例来帮助处理 CloudFront 缓存失效。在 OpenNext 中,只有当你进行按需重新验证(On Demand Revalidation)时才需要这样做。
此外,默认的 Next.js 独立输出(或 next start)在无服务器环境中无法工作,因为它试图在后台进行重新验证。
默认的 Next.js 还使用文件系统来缓存文件。你可以通过提供自己的缓存实现来覆盖它。在使用 OpenNext 时,这是自动完成的。
Next.js 设置的默认 Cache-Control 头也存在 2 个问题。
他们默认使用这个头 s-maxage=YOUR_REVALIDATION_TIME, stale-while-revalidate。这有 2 个问题:
stale-while-revalidate不是Cache-Control头的正确语法。它应该是stale-while-revalidate=TIME_WHERE_YOU_SERVE_STALE。他们在最近版本的 Next 中添加了此 未记录的选项 (opens in a new tab) 来补救这个问题- 为同一页面的每个请求设置相同的
s-maxage值可能不是一个好主意。 Next 可以根据你是请求完整 HTML 还是进行客户端导航(页面路由器的 RSC 或 JSON)来提供不同的内容。 这可能导致 ISR 缓存不一致,尤其是当你有很长的重新验证时间时。 例如,假设你使用 app router,你在主导航栏中有一个指向首页的链接,并将 ISR 设置为 1 天。其他每个页面都将有一个不同的 RSC 缓存条目(用于客户端导航)。这将导致你的应用中有多少页面就有多少缓存条目,它们都将具有相同的 1 天s-maxage值,但可能是在非常不同的时间被请求的。这可能导致某些页面提供长达 2 天的过时内容。
所有这些问题在 OpenNext 中都会自动为你修复
CloudFront 缓存失效
当你手动重新验证特定页面的 Next.js 缓存时,存储在 S3 上的 ISR 缓存文件将被更新。但是,仍然需要使 CloudFront 缓存失效:
// pages/api/revalidate.js
export default async function handler(req, res) {
await res.revalidate("/foo");
await invalidateCloudFrontPaths(["/foo"]);
// ...
}如果使用的是 pages router,你还必须使 _next/data/BUILD_ID/foo.json 路径失效。BUILD_ID 的值可以在 .next/BUILD_ID 构建输出中找到,并且可以在运行时通过 process.env.NEXT_BUILD_ID 环境变量访问。
await invalidateCloudFrontPaths(["/foo", `/_next/data/${process.env.NEXT_BUILD_ID}/foo.json`]);下面是 invalidateCloudFrontPaths() 函数的一个示例:
import { CloudFrontClient, CreateInvalidationCommand } from "@aws-sdk/client-cloudfront";
const cloudFront = new CloudFrontClient({});
async function invalidateCloudFrontPaths(paths: string[]) {
await cloudFront.send(
new CreateInvalidationCommand({
// 在此处设置 CloudFront 分配 ID
DistributionId: distributionId,
InvalidationBatch: {
CallerReference: `${Date.now()}`,
Paths: {
Quantity: paths.length,
Items: paths,
},
},
})
);
}请注意,手动 CloudFront 路径失效会产生费用。根据 AWS CloudFront 定价页面 (opens in a new tab):
每月前 1,000 个请求失效的路径不收取额外费用。此后,每个请求失效的路径收费 0.005 美元。
由于这些成本,如果多个路径需要失效,使通配符路径 /* 失效更经济。例如:
// 在前 1000 个路径之后,这将花费 $0.005 x 3 = $0.015
await invalidateCloudFrontPaths(["/page/a", "/page/b", "/page/c"]);
// 这将花费 $0.005,但也会使其他路由(如 "page/d")失效
await invalidateCloudFrontPaths(["/page/*"]);对于通过 next/cache 模块 (opens in a new tab) 进行的按需重新验证,如果你想检索给定标签的关联路径,你可以使用此函数:
import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({ region: process.env.CACHE_BUCKET_REGION });
async function getPaths(tag: string) {
try {
const { Items } = await client.send(
new QueryCommand({
TableName: process.env.CACHE_DYNAMO_TABLE,
KeyConditionExpression: "#tag = :tag",
ExpressionAttributeNames: {
"#tag": "tag",
},
ExpressionAttributeValues: {
":tag": { S: `${process.env.NEXT_BUILD_ID}/${tag}` },
},
})
);
return Items?.map((item) => item.path?.S?.replace(`${process.env.NEXT_BUILD_ID}/`, "") ?? "") ?? [];
} catch (e) {
console.error("Failed to get by tag", e);
return [];
}
}