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 />
}
要使用数据、cookies、headers、当前时间或随机值,您现在有一个选择:您希望数据被缓存(服务器端或客户端)还是在每次请求时执行?我以 fetch()
为例,但这适用于任何异步 Node API,例如数据库或计时器。
动态
如果您仍在迭代或构建高度动态的仪表板,您可以将组件包装在 <Suspense>
边界中。<Suspense>
选择加入动态数据获取和流式处理。
async function Component() {
return fetch(...) // no error
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}
您也可以在根布局或使用 loading.tsx
中执行此操作。
这确保了您的应用程序的外壳保持即时响应。您可以继续在 Page 中添加更多数据,知道它默认情况下将是动态的。默认情况下,没有任何内容被缓存。不再有隐藏的缓存。
静态
如果您正在构建静态内容,并且不想使用动态功能,则可以使用新的 use cache
指令。
"use cache"
export default async function Page() {
return fetch(...) // no error
}
通过使用 use cache
标记 Page,您表明整个段应被缓存。这意味着您获取的任何数据现在都可以被缓存,从而允许页面静态渲染。静态内容不使用 <Suspense>
边界。您可以向页面添加更多数据,所有数据都将被缓存。
部分
您也可以混合搭配使用。例如,您可以在根布局中放置 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>
}
同时在特定的 Page 中使用动态数据
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
添加到任何异步函数。将其视为服务器操作,但您不是调用服务器,而是调用缓存。它支持超出 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');
}
然后,像以前一样从服务器操作中调用 revalidateTag('my-tag')
即可。
由于此 API 可以在数据加载后调用,因此您现在可以使用数据来标记您的缓存条目。
import { unstable_cacheTag as 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 { unstable_cacheLife as 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;