跳到内容
返回博客

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 server 一样将 use cache 添加到任何异步函数。将其视为服务器操作,但您不是调用服务器,而是调用缓存。它支持超出 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 的信息。