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

注意事項

基於輸入參數優化快取回傳值的技術稱為記憶化(Memoization)。我們將 `cache` 回傳的函式稱為記憶化函式。

注意事項

  • React 會在每次伺服器請求時,將所有記憶化函式的快取失效。
  • 每次呼叫 `cache` 都會建立一個新的函式。這表示多次使用相同的函式呼叫 `cache` 將會回傳不同的記憶化函式,它們並不共享相同的快取。
  • `cachedFn` 也會快取錯誤。如果 `fn` 針對某些參數拋出錯誤,該錯誤將會被快取,並且當使用相同的參數呼叫 `cachedFn` 時,會重新拋出相同的錯誤。
  • `cache` 僅供在 伺服器元件(Server Components) 中使用。

用法

快取耗時的計算 ...

使用 `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` 物件在 `Profile` 和 `TeamReport` 中都被渲染,這兩個元件可以共享工作,並且只針對該 `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);
// ...
}

在上面的例子中,`Precipitation``Temperature` 各自呼叫 `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 都渲染相同的 城市,它們將會從 記憶函式 收到相同的資料快照。

如果 AnimatedWeatherCardMinimalWeatherCard 提供不同的 城市 參數給 getTemperature,那麼 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 已經完成,無論是錯誤還是 *已實現* 的結果,await 都會立即返回該值。在這兩種情況下,效能都會有所提升。

陷阱

在元件外部呼叫 memoized 函式不會使用快取。
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 時,它仍然會執行該函式,但不會讀取或更新快取。

這是因為快取存取是透過 Context 提供的,而 Context 只能從元件內部存取。

深入探討

我應該何時使用 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 的 memoized 值。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 類似,memoized 元件只會快取最後一次渲染和最後一次 prop 值。一旦 props 更改,快取就會失效,元件就會重新渲染。


疑難排解

即使我使用相同的參數呼叫我的 memoized 函式,它仍然會執行

請參閱前面提到的陷阱

如果以上皆不適用,則可能是 React 檢查快取中是否存在某項內容的方式出了問題。

如果您的參數不是基本類型(例如:物件、函式、陣列),請確保您傳遞的是相同的物件參考。

呼叫 memoized 函式時,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} />
</>
);
}