React Server Components

cache 仅用于 React Server Components

cache 让你缓存数据获取或计算的结果。

const cachedFn = cache(fn);

参考

🌐 Reference

cache(fn)

在任何组件之外调用 cache 以创建一个具有缓存功能的函数版本。

🌐 Call cache outside of any components to create a version of the function with caching.

import {cache} from 'react';
import calculateMetrics from 'lib/metrics';

const getMetrics = cache(calculateMetrics);

function Chart({data}) {
const report = getMetrics(data);
// ...
}

getMetrics 首次使用 data 调用时,getMetrics 会调用 calculateMetrics(data) 并将结果存储在缓存中。如果 getMetrics 再次使用相同的 data 调用,它将返回缓存的结果,而不是再次调用 calculateMetrics(data)

🌐 When getMetrics is first called with data, getMetrics will call calculateMetrics(data) and store the result in cache. If getMetrics is called again with the same data, it will return the cached result instead of calling calculateMetrics(data) again.

查看更多示例。

参数

🌐 Parameters

  • fn:你想要缓存结果的函数。fn 可以接受任何参数并返回任何值。

返回

🌐 Returns

cache 返回与 fn 具有相同类型签名的缓存版本。在此过程中,它不会调用 fn

在使用给定参数调用 cachedFn 时,它首先检查缓存中是否存在缓存的结果。如果存在缓存结果,它就返回该结果。如果不存在,则使用这些参数调用 fn,将结果存储在缓存中,并返回该结果。只有在缓存未命中的情况下才会调用 fn

🌐 When calling cachedFn with given arguments, it first checks if a cached result exists in the cache. If a cached result exists, it returns the result. If not, it calls fn with the arguments, stores the result in the cache, and returns the result. The only time fn is called is when there is a cache miss.

注意

基于输入优化缓存返回值的过程被称为记忆化。我们将从cache返回的函数称为记忆化函数。

🌐 The optimization of caching return values based on inputs is known as memoization. We refer to the function returned from cache as a memoized function.

注意事项

🌐 Caveats

  • React 将使每个服务器请求的所有记忆函数的缓存失效。
  • 每次调用 cache 都会创建一个新函数。这意味着多次使用相同函数调用 cache 将返回不同的记忆化函数,它们不会共享相同的缓存。
  • cachedFn 也会缓存错误。如果 fn 对某些参数抛出错误,它将被缓存,当使用相同参数调用 cachedFn 时,将重新抛出相同的错误。
  • cache 仅用于 服务器组件

用法

🌐 Usage

缓存昂贵的计算

🌐 Cache an expensive computation

使用 cache 来跳过重复的工作。

🌐 Use cache to skip duplicate work.

import {cache} from 'react';
import calculateUserMetrics from 'lib/user';

const getUserMetrics = cache(calculateUserMetrics);

function Profile({user}) {
const metrics = getUserMetrics(user);
// ...
}

function TeamReport({users}) {
for (let user in users) {
const metrics = getUserMetrics(user);
// ...
}
// ...
}

如果相同的 user 对象在 ProfileTeamReport 中都被渲染,这两个组件可以共享工作,并且只为该 user 调用一次 calculateUserMetrics

🌐 If the same user object is rendered in both Profile and TeamReport, the two components can share work and only call calculateUserMetrics once for that user.

假设首先渲染 Profile。它将调用 getUserMetrics,并检查是否有缓存结果。由于这是第一次使用该 user 调用 getUserMetrics,因此会发生缓存未命中。然后 getUserMetrics 将使用该 user 调用 calculateUserMetrics 并将结果写入缓存。

🌐 Assume Profile is rendered first. It will call getUserMetrics, and check if there is a cached result. Since it is the first time getUserMetrics is called with that user, there will be a cache miss. getUserMetrics will then call calculateUserMetrics with that user and write the result to cache.

TeamReport 渲染它的 users 列表并且到达相同的 user 对象时,它将调用 getUserMetrics 并从缓存中读取结果。

🌐 When TeamReport renders its list of users and reaches the same user object, it will call getUserMetrics and read the result from cache.

如果可以通过传入 AbortSignal 来中止 calculateUserMetrics,那么如果 React 已完成渲染,你可以使用 cacheSignal() 来取消昂贵的计算。calculateUserMetrics 可能已经通过直接使用 cacheSignal 内部处理了取消操作。

🌐 If calculateUserMetrics can be aborted by passing an AbortSignal, you can use cacheSignal() to cancel the expensive computation if React has finished rendering. calculateUserMetrics may already handle cancellation internally by using cacheSignal directly.

易犯错误

调用不同的记忆函数将从不同的缓存中读取。

🌐 Calling different memoized functions will read from different caches.

要访问相同的缓存,组件必须调用相同的记忆函数。

🌐 To access the same cache, components must call the same memoized function.

// Temperature.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export function Temperature({cityData}) {
// 🚩 Wrong: Calling `cache` in component creates new `getWeekReport` for each render
const getWeekReport = cache(calculateWeekReport);
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

// 🚩 Wrong: `getWeekReport` is only accessible for `Precipitation` component.
const getWeekReport = cache(calculateWeekReport);

export function Precipitation({cityData}) {
const report = getWeekReport(cityData);
// ...
}

在上面的例子中, PrecipitationTemperature 都调用了 cache 来创建一个带有各自缓存查找的新记忆化函数。如果两个组件为了相同的 cityData 渲染,它们将会重复调用 calculateWeekReport 进行工作。

🌐 In the above example, Precipitation and Temperature each call cache to create a new memoized function with their own cache look-up. If both components render for the same cityData, they will do duplicate work to call calculateWeekReport.

此外,Temperature 在每次组件渲染时都会创建一个 新的记忆函数 ,这不允许任何缓存共享。

为了最大化缓存命中率并减少工作量,这两个组件应该调用相同的记忆化函数来访问相同的缓存。相反,应在一个专用模块中定义该记忆化函数,以便可以在组件之间import-ed

🌐 To maximize cache hits and reduce work, the two components should call the same memoized function to access the same cache. Instead, define the memoized function in a dedicated module that can be import-ed across components.

// getWeekReport.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export default cache(calculateWeekReport);
// Temperature.js
import getWeekReport from './getWeekReport';

export default function Temperature({cityData}) {
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import getWeekReport from './getWeekReport';

export default function Precipitation({cityData}) {
const report = getWeekReport(cityData);
// ...
}

Here, both components call the same memoized function exported from ./getWeekReport.js to read and write to the same cache.

分享数据快照

🌐 Share a snapshot of data

要在组件之间共享数据快照,可以使用类似 fetch 的数据获取函数调用 cache。当多个组件进行相同的数据获取时,只会发出一次请求,返回的数据会被缓存并在组件之间共享。所有组件在服务器渲染期间引用的是相同的数据快照。

🌐 To share a snapshot of data between components, call cache with a data-fetching function like fetch. When multiple components make the same data fetch, only one request is made and the data returned is cached and shared across components. All components refer to the same snapshot of data across the server render.

import {cache} from 'react';
import {fetchTemperature} from './api.js';

const getTemperature = cache(async (city) => {
return await fetchTemperature(city);
});

async function AnimatedWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}

async function MinimalWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}

如果 AnimatedWeatherCardMinimalWeatherCard 都为同一个 城市渲染,它们将从 记忆化函数接收相同的数据快照。

如果 AnimatedWeatherCardMinimalWeatherCardgetTemperature 提供不同的 城市 参数 getTemperature,那么 fetchTemperature 将被调用两次,每个调用位置将接收到不同的数据。

城市 作为缓存键。

注意

异步渲染 仅支持服务器组件。

async function AnimatedWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}

要在客户端组件中渲染使用异步数据的组件,请参阅 use() 文档

🌐 To render components that use asynchronous data in Client Components, see use() documentation.

预加载数据

🌐 Preload data

通过缓存长时间运行的数据获取,你可以在渲染组件之前启动异步工作。

🌐 By caching a long-running data fetch, you can kick off asynchronous work prior to rendering the component.

const getUser = cache(async (id) => {
return await db.user.query(id);
});

async function Profile({id}) {
const user = await getUser(id);
return (
<section>
<img src={user.profilePic} />
<h2>{user.name}</h2>
</section>
);
}

function Page({id}) {
// ✅ Good: start fetching the user data
getUser(id);
// ... some computational work
return (
<>
<Profile id={id} />
</>
);
}

在渲染 Page 时,组件会调用 getUser ,但请注意它并未使用返回的数据。这个早期的 getUser 调用启动了异步数据库查询,而此时 Page 正在进行其他计算工作并渲染子组件。

🌐 When rendering Page, the component calls getUser but note that it doesn’t use the returned data. This early getUser call kicks off the asynchronous database query that occurs while Page is doing other computational work and rendering children.

在渲染 Profile 时,我们会再次调用 getUser 。如果最初的 getUser 调用已经返回并缓存了用户数据,当 Profile 请求并等待这些数据 时,它可以直接从缓存中读取,而无需另行进行远程过程调用。如果 最初的数据请求 尚未完成,这种模式下的预加载数据可以减少获取数据的延迟。

深入研究

缓存异步工作

🌐 Caching asynchronous work

在评估一个 异步函数 时,你将会收到一个对应该工作的 Promise。这个 Promise 保存了该工作的状态(待定已实现失败)以及其最终确定的结果。

🌐 When evaluating an asynchronous function, you will receive a Promise for that work. The promise holds the state of that work (pending, fulfilled, failed) and its eventual settled result.

在这个例子中,异步函数 fetchData 返回一个正在等待 fetch 的 promise。

🌐 In this example, the asynchronous function fetchData returns a promise that is awaiting the fetch.

async function fetchData() {
return await fetch(`https://...`);
}

const getData = cache(fetchData);

async function MyComponent() {
getData();
// ... some computational work
await getData();
// ...
}

在第一次调用 getData 时,从 fetchData 返回的 promise 会被缓存。随后的查询将返回相同的 promise。

🌐 In calling getData the first time, the promise returned from fetchData is cached. Subsequent look-ups will then return the same promise.

请注意,第一次 getData 调用不会await,而 第二次 则会。await 是一个 JavaScript 运算符,它会等待并返回该 promise 的最终结果。第一次 getData 调用只是启动 fetch,以便缓存该 promise 供第二次 getData 查找。

如果在 第二次调用 时,Promise仍然处于_pending_状态,则await将暂停以等待结果。优化之处在于,在等待fetch的同时,React可以继续进行计算工作,从而减少 第二次调用的等待时间。

如果承诺已经解决,无论是错误还是 fulfilled 结果,await 都会立即返回该值。在这两种情况下,都有性能上的好处。

易犯错误

在组件外部调用记忆函数将不会使用缓存。

🌐 Calling a memoized function outside of a component will not use the cache.

import {cache} from 'react';

const getUser = cache(async (userId) => {
return await db.user.query(userId);
});

// 🚩 Wrong: Calling memoized function outside of component will not memoize.
getUser('demo-id');

async function DemoProfile() {
// ✅ Good: `getUser` will memoize.
const user = await getUser('demo-id');
return <Profile user={user} />;
}

React 仅在组件中提供对 memoized 函数的缓存访问。当在组件外部调用 getUser 时,它仍然会评估函数,但不会读取或更新缓存。

🌐 React only provides cache access to the memoized function in a component. When calling getUser outside of a component, it will still evaluate the function but not read or update the cache.

这是因为缓存访问是通过一个 context 提供的,而该 context 只能从一个组件中访问。

🌐 This is because cache access is provided through a context which is only accessible from a component.

深入研究

我应该什么时候使用 cachememouseMemo

🌐 When should I use cache, memo, or useMemo?

所有提到的 API 都提供记忆功能,但区别在于它们要记忆的内容、谁可以访问缓存以及缓存何时失效。

🌐 All mentioned APIs offer memoization but the difference is what they’re intended to memoize, who can access the cache, and when their cache is invalidated.

useMemo

一般来说,你应该在客户端组件的多次渲染中使用 useMemo 来缓存昂贵的计算。例如,在组件内对数据转换进行记忆化。

🌐 In general, you should use useMemo for caching an expensive computation in a Client Component across renders. As an example, to memoize a transformation of data within a component.

'use client';

function WeatherReport({record}) {
const avgTemp = useMemo(() => calculateAvg(record), record);
// ...
}

function App() {
const record = getRecord();
return (
<>
<WeatherReport record={record} />
<WeatherReport record={record} />
</>
);
}

In this example, App renders two WeatherReports with the same record. Even though both components do the same work, they cannot share work. useMemo’s cache is only local to the component.

然而,useMemo 确实确保,如果 App 重新渲染且 record 对象没有变化,每个组件实例将跳过工作并使用 avgTemp 的缓存值。useMemo 只会缓存给定依赖下 avgTemp 的最后一次计算。

🌐 However, useMemo does ensure that if App re-renders and the record object doesn’t change, each component instance would skip work and use the memoized value of avgTemp. useMemo will only cache the last computation of avgTemp with the given dependencies.

cache

一般来说,你应该在服务器组件中使用 cache 来对可以在组件间共享的工作进行记忆化。

🌐 In general, you should use cache in Server Components to memoize work that can be shared across components.

const cachedFetchReport = cache(fetchReport);

function WeatherReport({city}) {
const report = cachedFetchReport(city);
// ...
}

function App() {
const city = "Los Angeles";
return (
<>
<WeatherReport city={city} />
<WeatherReport city={city} />
</>
);
}

Re-writing the previous example to use cache, in this case the second instance of WeatherReport will be able to skip duplicate work and read from the same cache as the first WeatherReport. Another difference from the previous example is that cache is also recommended for memoizing data fetches, unlike useMemo which should only be used for computations.

此时,cache 应仅在服务器组件中使用,并且缓存将在服务器请求之间失效。

🌐 At this time, cache should only be used in Server Components and the cache will be invalidated across server requests.

memo

如果组件的 props 没有变化,你应该使用 memo 来防止组件重新渲染。

🌐 You should use memo to prevent a component re-rendering if its props are unchanged.

'use client';

function WeatherReport({record}) {
const avgTemp = calculateAvg(record);
// ...
}

const MemoWeatherReport = memo(WeatherReport);

function App() {
const record = getRecord();
return (
<>
<MemoWeatherReport record={record} />
<MemoWeatherReport record={record} />
</>
);
}

在这个例子中,当首次渲染时,两个 MemoWeatherReport 组件都会调用 calculateAvg。然而,如果 App 重新渲染,并且 record 没有变化,则没有任何 props 发生变化,MemoWeatherReport 不会重新渲染。

🌐 In this example, both MemoWeatherReport components will call calculateAvg when first rendered. However, if App re-renders, with no changes to record, none of the props have changed and MemoWeatherReport will not re-render.

useMemo 相比,memo 根据 props 而不是特定计算来缓存组件渲染。类似于 useMemo,被缓存的组件只会缓存最后一次使用最后的 prop 值的渲染。一旦 props 发生变化,缓存就会失效,组件会重新渲染。

🌐 Compared to useMemo, memo memoizes the component render based on props vs. specific computations. Similar to useMemo, the memoized component only caches the last render with the last prop values. Once the props change, the cache invalidates and the component re-renders.


故障排除

🌐 Troubleshooting

即使我使用相同的参数调用了我的记忆函数,它仍然会运行

🌐 My memoized function still runs even though I’ve called it with the same arguments

请参阅前面提到的陷阱

🌐 See prior mentioned pitfalls

如果以上情况都不适用,则可能是 React 如何检查缓存中是否存在某些内容存在问题。

🌐 If none of the above apply, it may be a problem with how React checks if something exists in cache.

如果你的参数不是基本类型(例如对象、函数、数组),请确保你传递的是相同的对象引用。

🌐 If your arguments are not primitives (ex. objects, functions, arrays), ensure you’re passing the same object reference.

在调用记忆化函数时,React 会查找输入参数以查看是否已有缓存的结果。React 将使用参数的浅层相等性来确定是否命中缓存。

🌐 When calling a memoized function, React will look up the input arguments to see if a result is already cached. React will use shallow equality of the arguments to determine if there is a cache hit.

import {cache} from 'react';

const calculateNorm = cache((vector) => {
// ...
});

function MapMarker(props) {
// 🚩 Wrong: props is an object that changes every render.
const length = calculateNorm(props);
// ...
}

function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}

在这种情况下,这两个 MapMarker 看起来像是在做相同的工作,并以相同的 {x: 10, y: 10, z:10} 值调用 calculateNorm。即使这些对象包含相同的值,它们也不是相同的对象引用,因为每个组件都会创建自己的 props 对象。

🌐 In this case the two MapMarkers look like they’re doing the same work and calling calculateNorm with the same value of {x: 10, y: 10, z:10}. Even though the objects contain the same values, they are not the same object reference as each component creates its own props object.

React 将在输入上调用 Object.is 以验证是否有缓存命中。

🌐 React will call Object.is on the input to verify if there is a cache hit.

import {cache} from 'react';

const calculateNorm = cache((x, y, z) => {
// ...
});

function MapMarker(props) {
// ✅ Good: Pass primitives to memoized function
const length = calculateNorm(props.x, props.y, props.z);
// ...
}

function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}

解决这个问题的一种方法是将向量维度传递给 calculateNorm。这是可行的,因为维度本身是基本类型。

🌐 One way to address this could be to pass the vector dimensions to calculateNorm. This works because the dimensions themselves are primitives.

另一种解决方案可能是将向量对象本身作为属性传递给组件。我们需要将相同的对象传递给两个组件实例。

🌐 Another solution may be to pass the vector object itself as a prop to the component. We’ll need to pass the same object to both component instances.

import {cache} from 'react';

const calculateNorm = cache((vector) => {
// ...
});

function MapMarker(props) {
// ✅ Good: Pass the same `vector` object
const length = calculateNorm(props.vector);
// ...
}

function App() {
const vector = [10, 10, 10];
return (
<>
<MapMarker vector={vector} />
<MapMarker vector={vector} />
</>
);
}