我花了一个下午调试一个组件,该组件在每次请求时都会重新获取数据。
'use cache'代码里明明写着呢。我当时确信它能用。结果却不行。
问题出在位置上。'use cache'指令放在了包装函数里,而不是实际的数据函数里。这一个错误导致 Next.js 完全忽略了该指令。没有错误,没有警告,终端也没有任何输出。函数每次请求都会运行,而它本应该被缓存。
还有一次,我在 Next.js 16 迁移过程中,在服务器操作中编写了这段代码:
<span style="color:#37352f"><span style="background-color:#ffffff"><span style="color:#f8f8f2"><span style="color:#f8f8f2"><code><span style="color:#7ed07e">revalidateTag</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">'</span><span style="color:#f2ca27">products</span><span style="color:#f2ca27">'</span><span style="color:#f8f8f2">)</span>
</code></span></span></span></span>
编译成功,部署也完成了。但页面却不再反映变更。revalidateTag在 Next.js 16 中,调用函数时缺少第二个参数会导致 TypeScript 错误,但运行时却默默地回退到了旧版行为。直到用户开始报告数据过时,我才发现这个问题。
Next.js 16 的新缓存模型确实很棒。但在开发过程中,它完全是个黑盒。你添加了指令,就想当然地认为它能正常工作,只有当生产环境出现问题时,你才会发现并非如此。
所以我构建了一个仅供开发人员使用的小型工具包,使其可见。
它捕获的
1. 静默缓存未命中
如果一个函数使用相同的参数运行多次,你会立即看到这种情况:
<span style="color:#37352f"><span style="background-color:#ffffff"><span style="color:#f8f8f2"><span style="color:#f8f8f2"><code>[cache-debug] ⚠ POSSIBLE CACHE MISS - RE-EXECUTION WITH SAME ARGS
fn: getProductById
args: ["prod-123"]
This function ran 2 times with identical args.
If you expect caching: check 'use cache' is inside this function, not the wrapper.
</code></span></span></span></span>
早知道有那个警告,我整个下午就不用那么费劲了。
2. 缓存生命周期短导致的动态漏洞
cacheLife('seconds')系统会静默地将某个组件从 PPR 静态 shell 中移除,使其完全动态化。没有任何警告,页面加载速度明显变慢,但原因不明。
<span style="color:#37352f"><span style="background-color:#ffffff"><span style="color:#f8f8f2"><span style="color:#f8f8f2"><code>[cache-debug] ⚡ DYNAMIC HOLE WARNING
fn: getLivePrice
cacheLife 'seconds' is short-lived (< 5 minutes or revalidate: 0).
Next.js 16 automatically EXCLUDES this from the PPR static shell.
This function will run at request time, it is NOT prerendered.
Fix: Use 'minutes' or longer if you want it in the static shell.
</code></span></span></span></span>
3. 缺少缓存标签
缓存函数没有缓存cacheTag()机制,只能通过时间过期。你无法按需重新验证它。快速操作时很容易忽略,事后发现则非常麻烦。
<span style="color:#37352f"><span style="background-color:#ffffff"><span style="color:#f8f8f2"><span style="color:#f8f8f2"><code>[cache-debug] 🏷 MISSING cacheTag WARNING
fn: getProductById
No cacheTag() found. This function cannot be invalidated on demand.
It will only expire when cacheLife runs out.
Fix: Add cacheTag('your-tag') inside the function.
</code></span></span></span></span>
4. 已弃用的 revalidateTag
在 Next.js 16 中,revalidateTag('tag')缺少第二个参数会导致 TypeScript 错误。该logInvalidation辅助函数会在 CI 处理之前捕获到此错误:
<span style="color:#37352f"><span style="background-color:#ffffff"><span style="color:#f8f8f2"><span style="color:#f8f8f2"><code>[cache-debug] ✗ DEPRECATED revalidateTag - MISSING SECOND ARG
tag: products
revalidateTag('products') without a profile is deprecated in Next.js 16.
Fix: revalidateTag('products', 'max')
</code></span></span></span></span>
5. 在服务器操作之外更新标签
updateTag在服务器操作之外调用此方法会在运行时抛出异常。该工具包会在开发阶段捕获此异常,防止其进入生产环境。
6. 重复取取
detectRepeatedFetch如果在一次渲染过程中多次访问同一个 URL,通常意味着缓存层完全缺失。
如何使用它
步骤 1:启用.env.local
<span style="color:#37352f"><span style="background-color:#ffffff"><span style="color:#f8f8f2"><span style="color:#f8f8f2"><code><span style="color:#f8f8f2">CACHE_DEBUG</span><span style="color:#f9690e">=</span><span style="color:#f8f8f2">true</span>
</code></span></span></span></span>
请勿添加此项.env.production。NODE_ENV虽然生产环境中已有机制确保其关闭,但保持环境变量文件的整洁是一种良好的实践。
步骤 2:封装缓存函数
该'use cache'指令必须保留在原始函数内部。`is`withCacheDebug是一个普通的包装器,不能作为缓存边界。如果将其放在'use cache'包装器上,则缓存的是检测数据而不是数据函数,这正是“可能缓存未命中”警告旨在捕获的错误。
<span style="color:#37352f"><span style="background-color:#ffffff"><span style="color:#f8f8f2"><span style="color:#f8f8f2"><code><span style="color:#f39c12">import</span> <span style="color:#f8f8f2">{</span> <span style="color:#7ed07e">cacheLife</span><span style="color:#f8f8f2">,</span> <span style="color:#7ed07e">cacheTag</span> <span style="color:#f8f8f2">}</span> <span style="color:#f39c12">from</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">next/cache</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">;</span>
<span style="color:#f39c12">import</span> <span style="color:#f8f8f2">{</span> <span style="color:#7ed07e">withCacheDebug</span> <span style="color:#f8f8f2">}</span> <span style="color:#f39c12">from</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">@/lib/cache-debug</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">;</span>
<span style="color:#f39c12">async</span> <span style="color:#f39c12">function</span> <span style="color:#7ed07e">_getProductById</span><span style="color:#f8f8f2">(</span><span style="color:#7ed07e">id</span><span style="color:#f8f8f2">:</span> <span style="color:#f39c12">string</span><span style="color:#f8f8f2">)</span> <span style="color:#f8f8f2">{</span>
<span style="color:#f2ca27">"</span><span style="color:#f2ca27">use cache</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">;</span>
<span style="color:#7ed07e">cacheLife</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">"</span><span style="color:#f2ca27">hours</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">);</span>
<span style="color:#7ed07e">cacheTag</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">`product-</span><span style="color:#f8f8f2">${</span><span style="color:#7ed07e">id</span><span style="color:#f8f8f2">}</span><span style="color:#f2ca27">`</span><span style="color:#f8f8f2">,</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">products</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">);</span>
<span style="color:#f39c12">return</span> <span style="color:#7ed07e">db</span><span style="color:#f8f8f2">.</span><span style="color:#7ed07e">query</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">"</span><span style="color:#f2ca27">SELECT * FROM products WHERE id = $1</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span> <span style="color:#f8f8f2">[</span><span style="color:#7ed07e">id</span><span style="color:#f8f8f2">]);</span>
<span style="color:#f8f8f2">}</span>
<span style="color:#f39c12">export</span> <span style="color:#f39c12">const</span> <span style="color:#7ed07e">getProductById</span> <span style="color:#f9690e">=</span> <span style="color:#7ed07e">withCacheDebug</span><span style="color:#f8f8f2">(</span><span style="color:#7ed07e">_getProductById</span><span style="color:#f8f8f2">,</span> <span style="color:#f8f8f2">{</span>
<span style="color:#7ed07e">name</span><span style="color:#f8f8f2">:</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">getProductById</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span>
<span style="color:#7ed07e">cacheLife</span><span style="color:#f8f8f2">:</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">hours</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span>
<span style="color:#7ed07e">tags</span><span style="color:#f8f8f2">:</span> <span style="color:#f8f8f2">[</span><span style="color:#f2ca27">"</span><span style="color:#f2ca27">product-{id}</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">products</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">],</span>
<span style="color:#f8f8f2">});</span>
<span style="color:#f39c12">const</span> <span style="color:#7ed07e">product</span> <span style="color:#f9690e">=</span> <span style="color:#f39c12">await</span> <span style="color:#7ed07e">getProductById</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">"</span><span style="color:#f2ca27">prod-123</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">);</span>
</code></span></span></span></span>
API 没有任何变化。导出的函数在所有你已调用它的地方都完全相同。
步骤 3:在服务器操作中记录失效调用
<span style="color:#37352f"><span style="background-color:#ffffff"><span style="color:#f8f8f2"><span style="color:#f8f8f2"><code><span style="color:#f2ca27">"</span><span style="color:#f2ca27">use server</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">;</span>
<span style="color:#f39c12">import</span> <span style="color:#f8f8f2">{</span> <span style="color:#7ed07e">revalidateTag</span><span style="color:#f8f8f2">,</span> <span style="color:#7ed07e">updateTag</span> <span style="color:#f8f8f2">}</span> <span style="color:#f39c12">from</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">next/cache</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">;</span>
<span style="color:#f39c12">import</span> <span style="color:#f8f8f2">{</span> <span style="color:#7ed07e">logInvalidation</span> <span style="color:#f8f8f2">}</span> <span style="color:#f39c12">from</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">@/lib/cache-debug</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">;</span>
<span style="color:#f39c12">export</span> <span style="color:#f39c12">async</span> <span style="color:#f39c12">function</span> <span style="color:#7ed07e">updateProductPrice</span><span style="color:#f8f8f2">(</span><span style="color:#7ed07e">id</span><span style="color:#f8f8f2">:</span> <span style="color:#f39c12">string</span><span style="color:#f8f8f2">,</span> <span style="color:#7ed07e">newPrice</span><span style="color:#f8f8f2">:</span> <span style="color:#f39c12">number</span><span style="color:#f8f8f2">)</span> <span style="color:#f8f8f2">{</span>
<span style="color:#f39c12">await</span> <span style="color:#7ed07e">db</span><span style="color:#f8f8f2">.</span><span style="color:#7ed07e">query</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">"</span><span style="color:#f2ca27">UPDATE products SET price = $1 WHERE id = $2</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span> <span style="color:#f8f8f2">[</span><span style="color:#7ed07e">newPrice</span><span style="color:#f8f8f2">,</span> <span style="color:#7ed07e">id</span><span style="color:#f8f8f2">]);</span>
<span style="color:#7ed07e">logInvalidation</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">"</span><span style="color:#f2ca27">updateTag</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span> <span style="color:#f2ca27">`product-</span><span style="color:#f8f8f2">${</span><span style="color:#7ed07e">id</span><span style="color:#f8f8f2">}</span><span style="color:#f2ca27">`</span><span style="color:#f8f8f2">,</span> <span style="color:#f8f8f2">{</span>
<span style="color:#7ed07e">isServerAction</span><span style="color:#f8f8f2">:</span> <span style="color:#f39c12">true</span><span style="color:#f8f8f2">,</span>
<span style="color:#7ed07e">context</span><span style="color:#f8f8f2">:</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">admin price update</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span>
<span style="color:#f8f8f2">});</span>
<span style="color:#7ed07e">updateTag</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">`product-</span><span style="color:#f8f8f2">${</span><span style="color:#7ed07e">id</span><span style="color:#f8f8f2">}</span><span style="color:#f2ca27">`</span><span style="color:#f8f8f2">);</span>
<span style="color:#7ed07e">logInvalidation</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">"</span><span style="color:#f2ca27">revalidateTag</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">products</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span> <span style="color:#f8f8f2">{</span>
<span style="color:#7ed07e">profile</span><span style="color:#f8f8f2">:</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">max</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span>
<span style="color:#7ed07e">isServerAction</span><span style="color:#f8f8f2">:</span> <span style="color:#f39c12">true</span><span style="color:#f8f8f2">,</span>
<span style="color:#7ed07e">context</span><span style="color:#f8f8f2">:</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">admin price update</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span>
<span style="color:#f8f8f2">});</span>
<span style="color:#7ed07e">revalidateTag</span><span style="color:#f8f8f2">(</span><span style="color:#f2ca27">"</span><span style="color:#f2ca27">products</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">,</span> <span style="color:#f2ca27">"</span><span style="color:#f2ca27">max</span><span style="color:#f2ca27">"</span><span style="color:#f8f8f2">);</span>
<span style="color:#f8f8f2">}</span>
</code></span></span></span></span>
诚实的局限性
- 进程级作用域。冷启动时执行映射重置。在无服务器环境中,每次调用都可能是一个全新的进程,因此您只能在同一个热启动实例中看到重新执行的数据。对于长时间运行服务器的本地开发环境,其工作方式完全符合预期。
- 尽力实现并发。在并发渲染且参数相同的情况下,两次调用都可能记录“首次运行”而不是“未命中”。这种检测方式不影响正确性。
- 无法检查 Next.js 的内部结构。调试器会统计执行次数以检测可能的缓存未命中。它无法直接读取 Next.js 的内部缓存存储。
由于该工具不在现场,因此这些都不会影响生产。
一览
| 它检测到了什么 | 如果没有这个工具 |
|---|---|
| 首次运行/缓存未命中/新密钥 | 不可见 |
| 短缓存寿命的动态漏洞 | 不可见 |
| 缺少缓存标签 | 不可见 |
| 已弃用的 revalidateTag | TypeScript错误,容易被忽略 |
| 服务器操作外部的 updateTag | 运行时抛出 |
| 一次渲染中重复获取数据 | 不可见 |
零外部依赖。TypeScript 5.0+,严格模式。仅限 Next.js 16—— updateTagNext.js 15 中不存在。采用双重NODE_ENV === 'development'AND逻辑CACHE_DEBUG=true,因此不会将任何内容发布到生产环境。无额外开销,不影响打包。
得到它
永久免费,仅一个.tsx文件:shubhra.dev/snippets/nextjs-use-cache-debugger
如果您想要与此配套的生产强制执行层——类型安全的标签注册表,safeRevalidate在编译时阻止已弃用的单参数调用,serverActionInvalidate强制执行正确的失效顺序——那就是Cache Pro Kit。
如果您是 Next.js 16 缓存模型的新手,并且想在使用此工具包之前了解 `cache` 'use cache'、 `cache_remove` 和 `cache_remove` 的实际作用,那么这份实用的迁移指南将为您提供全面的指导。此外,指南中还包含一个15 道题的测试,供您检验自己的理解。cacheLifecacheTag
如果您正在进行 Next.js 16 缓存迁移,并且遇到任何异常情况,此功能可以帮助您发现问题所在。对您而言,新缓存模型中最令人沮丧或困惑的部分是什么?


所有评论(0)