缓存 - This feature is available in the latest Canary

Canary 版本

cache 允许您缓存数据获取或计算的结果。

const cachedFn = cache(fn);

参考

cache(fn)

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

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) 并将结果存储在缓存中。如果再次使用相同的 data 调用 getMetrics,它将返回缓存的结果,而不是再次调用 calculateMetrics(data)

请参阅下面的更多示例。

参数

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

返回值

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

当使用给定参数调用 cachedFn 时,它首先检查缓存中是否存在缓存的结果。如果存在缓存的结果,则返回该结果。如果没有,它将使用参数调用 fn,将结果存储在缓存中,并返回该结果。仅当缓存未命中时才会调用 fn

注意

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

注意事项

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

用法

缓存耗时的计算

使用 cache 来跳过重复工作。

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

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

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

陷阱

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

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

// 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

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

要最大限度地提高缓存命中率并减少工作量,这两个组件应该调用相同的记忆函数来访问同一个缓存。相反,请在一个专用模块中定义记忆函数,该模块可以在组件之间进行 import 导入

// 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);
// ...
}

在这里,这两个组件都调用从 ./getWeekReport.js 导出的 相同的记忆函数 来对同一个缓存进行读取和写入操作。

共享数据快照

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

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 提供不同的 城市 参数,则 fetchTemperature 将被调用两次,并且每个调用位置将收到不同的数据。

城市 充当缓存键。

注意

异步渲染 仅适用于服务器组件。

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

预加载数据

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

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 执行其他计算工作和渲染子组件时启动异步数据库查询。

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

深入探讨

缓存异步工作

当评估一个异步函数时,您将收到一个表示该工作的Promise。Promise 包含该工作的状态(*pending*、*fulfilled*、*failed*)及其最终的解决结果。

在本例中,异步函数 fetchData 返回一个正在等待 fetch 的 Promise。

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

const getData = cache(fetchData);

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

在第一次调用 getData 时,从 fetchData 返回的 Promise 被缓存。后续的查找将返回相同的 Promise。

请注意,第一次 getData 调用没有 await,而第二次调用了。await 是一个 JavaScript 运算符,它将等待并返回 Promise 的解决结果。第一次 getData 调用只是简单地启动 fetch 来缓存 Promise,以便第二次 getData 进行查找。

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

如果 Promise 已经解决,无论是出现错误还是 *fulfilled* 结果,await 都会立即返回该值。在这两种结果中,性能都会有所提高。

陷阱

在组件外部调用记忆函数不会使用缓存。
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 仅在组件中提供对记忆函数的缓存访问。在组件外部调用 getUser 时,它仍然会评估该函数,但不会读取或更新缓存。

这是因为缓存访问是通过 上下文 提供的,而上下文只能从组件内部访问。

深入探讨

我应该何时使用 cachememouseMemo

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

useMemo

通常,您应该使用 useMemo 在客户端组件的多次渲染之间缓存计算量大的结果。例如,记忆组件内的数据转换。

'use client';

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

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

在此示例中,App 使用相同的记录渲染了两个 WeatherReport。即使两个组件执行相同的工作,它们也不能共享工作成果。useMemo 的缓存仅对组件本地有效。

但是,useMemo 确实可以确保如果 App 重新渲染并且 record 对象没有更改,则每个组件实例都会跳过工作并使用 avgTemp 的记忆值。useMemo 只会缓存具有给定依赖项的 avgTemp 的最后一次计算结果。

cache

通常,您应该在服务器组件中使用 cache 来记忆可以在组件之间共享的工作成果。

const cachedFetchReport = cache(fetchReport);

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

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

将前面的示例重写为使用 cache,在本例中,WeatherReport 的第二个实例 将能够跳过重复工作并从与 WeatherReport 的第一个实例相同的缓存中读取数据。与前一个示例的另一个区别是,cache 也被推荐用于 记忆数据获取,这与 useMemo 不同,后者应该仅用于计算。

目前,cache 应该只在服务器组件中使用,并且缓存将在服务器请求之间失效。

memo

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

'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 将不会重新渲染。

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


故障排除

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

请参阅前面提到的陷阱

如果以上情况均不适用,则可能是 React 检查缓存中是否存在内容的方式存在问题。

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

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

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 对象。

React 会调用输入上的 Object.is 来验证是否存在缓存命中。

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。这种方法可行,因为维度本身是原始值。

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

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} />
</>
);
}