Next.js 和 OpenNext 中的缓存机制
在 Vercel 之外使用 Next.js 时,缓存问题可能会迅速变得复杂。有许多因素需要考虑。
通常情况下,您会在 Next.js 应用前部署 CDN。这个 CDN 会缓存来自 Next.js 应用的响应并分发给用户。这对性能很有帮助,但当需要使缓存失效时也可能带来问题。我们提供了一些代码示例来帮助处理 CloudFront 缓存失效。在 OpenNext 中,只有当你使用按需重新验证(On Demand Revalidation)时才需要这样做。
此外,默认的 Next.js 独立输出(或 next start
)在无服务器环境中无法正常工作,因为它试图在后台执行重新验证。
默认的 Next.js 还使用文件系统来缓存文件。您可以通过提供自己的缓存实现来覆盖它。使用 OpenNext 时,这会被自动完成。
Next.js 设置的默认 Cache-Control
头也存在两个问题:
默认使用这个头部:s-maxage=YOUR_REVALIDATION_TIME, stale-while-revalidate
。这里有两个问题:
stale-while-revalidate
不是Cache-Control
头的正确语法。应该是stale-while-revalidate=TIME_WHERE_YOU_SERVE_STALE
。他们在最新版本的 Next.js 中添加了这个未记录的选项 (opens in a new tab)来补救这个问题- 对同一页面的每个请求设置相同的
s-maxage
值可能不是个好主意。 Next.js 可以根据您请求的是完整 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 路由,还必须使 _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 [];
}
}