AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Next Start: A Page Revalidated With RevalidateTag Is Still Served For A Few Ms, Because Tag Expiry Is Stamped With Date.Now() But Checked Against Performance.Now()

在 next start 使用文件系统缓存时,revalidateTag 立即过期标签后,由于写入过期时间使用 Date.now()(墙上时钟),而读取判断使用 performance.timeOrigin + performance.now()(单调时钟),两者存在进程启动时的几毫秒偏移,导致过期标签在短时间内仍被视为未过期,从而继续提供旧缓存页面。

mediumConfidence 95%Next.jsAffected V16.3.8

Origin Analysis

FileSystemCache.revalidateTag 使用 Date.now() 写入 expired 字段,而 areTagsExpired 使用 performance.timeOrigin + performance.now() 获取当前时间进行比较。两个时钟源在进程启动时存在几毫秒的固有偏移,导致刚写入的过期时间被误判为未来时间。
1. 安装 next@16.3.8; 2. 创建 repro.cjs,内容如下: ```js const { areTagsExpired, tagsManifest } = require("next/dist/server/lib/incremental-cache/tags-manifest.external.js"); const cachedAt = Date.now() - 1000; tagsManifest.set("pages", { expired: Date.now() }); const real = performance.now.bind(performance); performance.now = () => real() - 50; console.log(areTagsExpired(["pages"], cachedAt)); // 输出 false,表示标签未过期 ``` 3. 运行 node repro.cjs,观察到输出 false,而预期应为 true(标签已过期)。

Fixing Code Block

export function areTagsExpired(tags: string[], cachedAt: number): boolean { // 使用两个时钟源中较大的值,确保刚写入的过期时间立即生效 const now = Math.max(Date.now(), performance.timeOrigin + performance.now()); for (const tag of tags) { const manifest = tagsManifest.get(tag); if (!manifest) continue; // 如果标签的过期时间在未来,并且晚于缓存时间,则标签未过期 if (manifest.expired > now && manifest.expired > cachedAt) { return false; } } return true; }
在 areTagsExpired 中,将当前时间的获取改为取 Date.now() 与 performance.timeOrigin + performance.now() 两者中的较大值。这样无论哪个时钟源更靠前,刚写入的过期时间都不会被误判为未来,从而保证 revalidateTag 后标签立即过期。该修复同时兼容使用单调时钟的默认缓存处理器和文件系统缓存。

Edge Case Audit

使用 Math.max 会导致过期时间最多提前几毫秒触发,这是可接受的。但需注意:1) 如果系统时钟被手动向前调整,可能导致所有标签被误判为过期,建议在系统时钟剧烈变化时重新启动服务或重置缓存;2) 该修改会影响所有调用 areTagsExpired 的模块,需回归测试文件系统缓存和默认 use cache 场景。回滚方案:若出现异常,恢复原代码(const now = performance.timeOrigin + performance.now())即可。

Ecosystem Topology