跳到内容
返回博客

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

选择你的冒险

您首先会注意到,当您向组件添加数据时,您现在会收到错误。

app/page.tsx
async function Component() {
  return fetch(...) // error
}
 
export default async function Page() {
  return <Component />
}

要使用数据、cookies、headers、当前时间或随机值,您现在可以选择:您希望数据被缓存(服务器端或客户端)还是在每次请求时执行?我使用 fetch() 作为示例,但这适用于任何异步 Node API,例如数据库或计时器。

动态

如果您仍在迭代或构建高度动态的仪表板,您可以将组件包裹在 <Suspense> 边界中。<Suspense> 选择加入动态数据获取和流式传输。

app/page.tsx
async function Component() {
  return fetch(...) // no error
}
 
export default async function Page() {
  return <Suspense fallback="..."><Component /></Suspense>
}

您也可以在根布局或使用 loading.tsx 中执行此操作。

这确保了您的应用程序外壳保持即时响应。您可以继续在 Page 中添加更多数据,了解它默认情况下将全部是动态的。默认情况下不缓存任何内容。不再有隐藏的缓存。

静态

如果您正在构建静态内容并且不想使用动态功能,则可以使用新的 use cache 指令。

app/page.tsx
"use cache"
 
export default async function Page() {
  return fetch(...) // no error
}

通过使用 use cache 标记 Page,您表明整个段应被缓存。这意味着您获取的任何数据现在都可以被缓存,从而允许页面静态渲染。静态内容不使用 <Suspense> 边界。您可以向页面添加更多数据,所有数据都将被缓存。

部分

您也可以混合搭配。例如,您可以在根布局中放置 use cache 以确保其被缓存。每个布局或页面都可以独立缓存。

app/layout.tsx
"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 中使用动态数据

app/page.tsx
import { Suspense } from 'react'
async function Component() {
  return fetch(...) // no error
}
 
export default async function Page() {
  return <Suspense fallback="..."><Component /></Suspense>
}

缓存函数

当使用这种混合方法时,在 API 调用附近添加缓存可能更方便。

您可以将 use cache 添加到任何异步函数,就像 use server 一样。将其视为服务器操作,但您调用的不是服务器,而是缓存。它支持超出 JSON 的相同丰富的参数和返回值类型。缓存键自动包含任何参数和闭包,因此您无需手动指定缓存键。

app/layout.tsx
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。

app/utils.ts
import { cacheTag } from 'next/cache';
 
async function getNotice() {
  'use cache';
  cacheTag('my-tag');
}

然后,像以前一样从服务器操作中调用 revalidateTag('my-tag')

由于此 API 可以在数据加载后调用,您现在可以使用数据来标记您的缓存条目。

app/actions.ts
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

app/page.tsx
"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 标志

next.config.ts
import type { NextConfig } from 'next';
 
const nextConfig: NextConfig = {
  experimental: {
    dynamicIO: true,
  }
};
 
export default nextConfig;

在我们的文档中阅读更多关于 use cachecacheLifecacheTag 的信息。