cache - This feature is available in the latest Canary

React 服务端组件

cache 仅用于 React 服务端组件

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

当第一次使用data调用getMetrics时,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 都会创建一个 新的记忆化函数,这不允许任何缓存共享。

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

// 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 都针对相同的 city 进行渲染,它们将从 记忆化函数 中接收相同的数据快照。

如果 AnimatedWeatherCardMinimalWeatherCardcity 参数传递了不同的值给 getTemperature,则 fetchTemperature 将被调用两次,并且每个调用点都将接收不同的数据。

city 充当缓存键。

注意

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

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 保持着该工作的状态(pendingfulfilledfailed)及其最终的确定结果。

在此示例中,异步函数 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

你应该使用memo 来防止组件在其 props 未更改时重新渲染。

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