在独立模式下,Next.js 会在构建过程中预生成 ISR 缓存。运行时,NextServer 期望这些缓存在服务器本地可用。当服务器运行在单台 Web 服务器机器上时,这种方式能有效地在所有请求间共享缓存。但在 Lambda 环境中,缓存需要集中存储在所有服务器 Lambda 函数实例都能访问的位置,而 S3 正是作为这个集中存储位置。
为实现这一机制:
- ISR 缓存文件会被排除在
server-function
打包文件之外,转而上传到缓存桶中 - 通过在
next.config.js
中配置incrementalCacheHandlerPath
(opens in a new tab) 字段,替换默认的缓存处理器为自定义缓存处理器 - 自定义缓存处理器负责管理 S3 上的缓存文件,处理读写操作
- 由于我们使用 FIFO 队列,如果需要同时处理多个重新验证请求,就需要不同的消息组 ID。我们基于路由路径为每个重新验证请求生成消息组 ID,确保同一路由的重新验证请求只会被处理一次。您可以通过
MAX_REVALIDATE_CONCURRENCY
环境变量控制同时处理的重新验证请求数量,默认值为 10 revalidation-function
会从队列中轮询消息,并向路由发送带有x-prerender-revalidate
请求头的HEAD
请求server-function
接收到HEAD
请求后会重新验证缓存- 标签(tags)在 DynamoDB 表中采用不同的处理方式。我们使用单独的表来存储每个路由的标签,自定义缓存处理器在更新缓存时会同步更新表中的标签
ISR(增量静态再生)请求的生命周期(针对过期页面)
- Cloudfront 接收到一个页面请求。假设该页面在 Cloudfront 中已过期。
- Cloudfront 在后台将请求转发给
server-function
,但仍返回缓存的版本。 server-function
检查 S3 缓存。如果页面已过期,它会将过期响应返回给 Cloudfront,同时向重新验证队列发送消息以触发后台重新验证。它还会将 cache-control 标头更改为s-maxage=2, stale-while-revalidate=2592000
。- 2 秒后,同一个页面收到新的请求。Cloudfront 将缓存版本返回给用户,并将请求转发给
server-function
。 - 如果重新验证已完成,
server-function
将更新缓存并将更新后的响应发送回 Cloudfront。后续请求将获得更新后的版本。否则,我们将回到第 3 步。
标签(Tags)
标签存储在 dynamodb 表中。
表中有 3 个字段:tag
、path
、revalidatedAt
。tag
字段是分区键,path
是排序键。
我们使用名为 revalidate
的索引,其中 path
作为分区键,revalidatedAt
作为排序键。
每个标签都有多个路径,每个子路径也被视为一个标签。例如,如果我们有一个标签 tag1
,其路径为 /a/b/c
,那么我们还有标签 /a
、/a/layout
、/a/page
、/a/b
、/a/b/layout
、/a/b/page
、/a/b/c/layout
、/a/b/c/page
。
当调用 revalidateTag
时,我们会更新与该标签关联的每个路径和子路径的 revalidatedAt
值。
当我们检查页面是否过期时,我们会检查每条记录的 revalidatedAt
值和 S3 缓存对象的 LastModified
。如果 revalidatedAt
大于 LastModified
,我们认为该页面已过期。
成本说明
请注意 fetch 缓存使用的是 S3 服务。Next.js 中默认情况下 fetch
会被缓存,即使是 SSR 请求也会写入 S3。这可能导致大量 S3 请求并产生较高费用。您可以通过在 fetch
选项中设置 cache
为 no-store
来禁用 fetch 缓存。另请参阅此解决方案
对于未被 Cloudfront 缓存的 ISR 和 SSG 请求,每次请求都会调用 get
方法,而每次重新验证时都会调用 set
方法。如果 fetch
请求未将 cache
选项设为 no-store
,这些方法也可能被调用。
部署时也会产生一些成本,因为需要将缓存上传到 S3 并将标签上传到 DynamoDB。
以下示例假设我们在 us-east-1 区域有一个重新验证间隔为 5 分钟的应用路由,且该路由持续有流量访问(如果没有流量,则只需支付存储费用)。
S3 成本
- 每次缓存
get
请求至少会产生 1 次GetObject
操作
GetObject 成本 - 8,640 次请求 * 每 1,000 次请求 $0.0004 = $0.003456
总成本 - 每月每条路由 $0.003456
- 每次缓存
set
请求会产生 1 次 S3PutObject
操作
PutObject 成本 - 8,640 次请求 * 每 1,000 次请求 $0.005 = $0.0432
总成本 - 每月每条路由 $0.0432
您可以根据实际使用情况和 S3 定价 (opens in a new tab) 计算具体成本。
DynamoDB 成本分析
以下示例分析假设某路由包含 2 个标签,每个标签有 10 条路径和子路径,且该路由有持续稳定的访问量。
- 每次
revalidateTag
请求会产生:- 1 次 DynamoDB
Query
操作 - 每个关联路径执行 1 次
PutItem
操作(通过BatchWriteItem
批量写入,每批最多 25 条)
- 1 次 DynamoDB
假设每 5 分钟执行 1 次重新验证:
Query 成本 - 8,640 次请求 * 每百万次读取 $0.25 = $0.00216
BatchWriteItem 成本 - 86,400 次请求 * 每百万次写入 $0.25 = $0.0216
每月总成本 - 每个标签重新验证 $0.04536
- 每次
get
请求会产生 1 次 DynamoDBQuery
操作
Query 成本 - 8,640 次请求 * 每百万次读取 $0.25 = $0.00216
每月总成本 - 每个路由 $0.00216
- 每次
set
请求会产生:- 1 次 DynamoDB
Query
操作 - 为路径关联的每个未存在于 DynamoDB 的标签执行 1 次
PutItem
操作(通过BatchWriteItem
批量写入,每批最多 25 条)
- 1 次 DynamoDB
Query 成本 - 8,640 次请求 * 每百万次读取 $0.25 = $0.00216
每月总成本 - 每个路由 $0.00216
您可以根据实际使用情况和 DynamoDB 定价 (opens in a new tab) 计算总成本。