AWS
缓存 (ISR/SSG)

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 [];
  }
}