2024年10月24日星期四
我们与缓存的旅程
发布者前端性能很难做到尽善尽美。即使在高度优化的应用程序中,最常见的罪魁祸首仍然是客户端-服务器瀑布式请求。当引入 Next.js App Router 时,我们知道我们想要解决这个问题。为此,我们需要使用 React Server Components 在一次往返中将客户端-服务器 REST 获取移动到服务器。这意味着服务器有时必须是动态的,牺牲了 Jamstack 出色的初始加载性能。我们构建了部分预渲染来解决这种权衡,并实现两全其美。
然而,在此过程中,由于我们提供的缓存默认值和控制,开发者体验受到了影响。`fetch()` 的默认设置改为默认缓存以优先考虑性能,但快速原型开发和高度动态的应用程序却因此受损。我们没有为不使用 `fetch()` 的本地数据库访问提供足够的控制。我们有 `unstable_cache()`,但它不符合人体工程学。这导致了需要段级别配置,例如 `export const dynamic, runtime, fetchCache, dynamicParams, revalidate = ...`,作为一种应急措施。
当然,我们将继续支持它以实现向后兼容。但此刻,我想让您忘记所有这些。我们认为我们有一个更简单的想法。
我们一直在酝酿一种新的实验模式,它基于两个概念:`<Suspense>` 和 `use cache`。
选择你的冒险
您会注意到的第一件事是,当您向组件添加数据时,现在会收到错误。
async function Component() {
return fetch(...) // error
}
export default async function Page() {
return <Component />
}要使用数据、cookie、头部、当前时间或随机值,您现在有两种选择:您希望数据被缓存(服务器端或客户端)还是在每个请求上执行?我以 `fetch()` 为例,但这适用于任何异步 Node API,例如数据库或定时器。
动态
如果您仍在迭代或构建高度动态的仪表板,您可以将组件包装在 `
async function Component() {
return fetch(...) // no error
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}您也可以在根布局中执行此操作,或者使用 `loading.tsx`。
这确保了应用程序的外壳保持即时。您可以继续在页面中添加更多数据,并知道所有数据都将默认是动态的。默认情况下不缓存任何内容。不再有隐藏的缓存。
静态
如果您正在构建静态内容且不想使用动态功能,您可以使用新的 `use cache` 指令。
"use cache"
export default async function Page() {
return fetch(...) // no error
}通过使用 `use cache` 标记页面,您表明整个段都应该被缓存。这意味着您获取的任何数据现在都可以被缓存,从而允许页面静态渲染。静态内容不使用 `
部分
您也可以混合搭配使用。例如,您可以在根布局中放置 `use cache` 以确保其被缓存。每个布局或页面都可以独立缓存。
"use cache"
export default async function Layout({ children }) {
const response = await fetch(...)
const data = await response.json()
return <html>
<body>
<div>{data.notice}</div>
{children}
</body>
</html>
}在特定页面中使用动态数据时
import { Suspense } from 'react'
async function Component() {
return fetch(...) // no error
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}缓存函数
当采用这种混合方法时,将缓存更靠近 API 调用可能更方便。
您可以像 `use server` 一样将 `use cache` 添加到任何异步函数中。将其视为 Server Action,但您不是调用 Server,而是调用 Cache。它支持与 Server Action 相同的丰富的参数类型和返回值,不仅仅是 JSON。缓存键会自动包含任何参数和闭包,因此您无需手动指定缓存键。
async function getNotice() {
"use cache"
const response = await fetch(...)
const data = await response.json()
return data.notice;
}
export default async function Layout({ children }) {
return <html>
<body>
<h1>{await getNotice()}</h1>
{children}
</body>
</html>
}由于此布局中没有使用其他数据,因此它可以保持静态。这种方法的一个好处是,如果您不小心向布局添加了新的动态数据,它将在构建期间触发错误,迫使您做出新的选择。如果您将 `use cache` 添加到整个布局中,它将被缓存而不会出现错误。您选择哪种方法取决于您的用例。
标记缓存
如果您想通过标签明确清除缓存条目,可以在 `use cache` 函数内部使用新的 `cacheTag()` API。
import { cacheTag } from 'next/cache';
async function getNotice() {
'use cache';
cacheTag('my-tag');
}然后,只需像以前一样从 Server Action 调用 `revalidateTag('my-tag')`。
由于此 API 可以在数据加载后调用,因此您现在可以使用数据来标记您的缓存条目。
import { cacheTag } from 'next/cache';
async function getBlogPosts(page) {
'use cache';
const posts = await fetchPosts(page);
for (let post of posts) {
cacheTag('blog-post-' + post.id);
}
return posts;
}定义缓存的生命周期
如果您想控制特定条目或页面在缓存中的生命周期,可以使用 `cacheLife()` API。
"use cache"
import { cacheLife } from 'next/cache'
export default async function Page() {
cacheLife("minutes")
return ...
}默认情况下,它接受以下值:
"seconds"(秒)"minutes"(分钟)"hours"(小时)"days"(天)"weeks"(周)"max"(最大)
选择一个最适合您用例的大致范围。无需指定精确的数字并计算一周中有多少秒(或者说是毫秒?)。但是,您也可以指定特定值或配置您自己的命名缓存配置文件。
除了 `revalidate` 之外,此 API 还可以控制客户端缓存的 `stale` 时间以及 `expire`,后者决定了页面在一段时间内没有太多流量时何时应该过期。
实验性
这仍然是一个非常实验性的项目。它尚未达到生产就绪状态,并且仍缺少功能和存在错误。特别是,我们知道我们需要改进这种新型错误的错误堆栈。但是,如果您喜欢冒险,我们非常欢迎您的早期反馈。
我们将发布更详细的升级路径。除了早期错误之外,这里主要的破坏性更改是撤销 `fetch()` 的默认缓存。也就是说,我们建议在此早期实验阶段仅在全新项目中进行实验。如果进展顺利,我们希望在次要版本中发布一个可选版本,并在未来的主要版本中使其成为默认设置。
要试用它,您必须使用 Next.js 的 `canary` 版本。
npx create-next-app@canary您还必须在 `next.config.ts` 中启用实验性的 `dynamicIO` 标志。
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
}
};
export default nextConfig;在我们的文档中阅读更多关于 `use cache`、`cacheLife` 和 `cacheTag` 的信息。