キャッシュ
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) を呼び出し、その結果をキャッシュに保存します。もし getMetrics が同じ data で再度呼び出されると、calculateMetrics(data) を再度呼び出す代わりにキャッシュされた結果を返します。
パラメータ
fn: 結果をキャッシュしたい関数。fnは任意の引数を取り、任意の値を返すことができます。
戻り値
cache は、同じ型シグネチャを持つ fn のキャッシュバージョンを返します。このプロセスでは fn は呼び出されません。
与えられた引数で cachedFn を呼び出すと、まずキャッシュにキャッシュされた結果が存在するかどうかを確認します。キャッシュされた結果が存在する場合、その結果を返します。存在しない場合、引数を使って fn を呼び出し、結果をキャッシュに保存し、その結果を返します。fn が呼び出されるのはキャッシュミスが発生したときだけです。
注意点
- 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 の両方でレンダーされる場合、2つのコンポーネントは作業を共有し、その user に対して calculateUserMetrics を一度だけ呼び出すことができます。
まず Profile がレンダーされると仮定します。それは getUserMetrics を呼び出し、キャッシュされた結果があるかどうかを確認します。その user で getUserMetrics が初めて呼び出されるので、キャッシュミスが発生します。getUserMetrics はその後、その user で calculateUserMetrics を呼び出し、結果をキャッシュに書き込みます。
TeamReport が users のリストをレンダーし、同じ user オブジェクトに到達すると、getUserMetrics を呼び出し、結果をキャッシュから読み取ります。
データのスナップショットを共有する
コンポーネント間でデータのスナップショットを共有するためには、fetch のようなデータ取得関数とともに cache を呼び出します。複数のコンポーネントが同じデータを取得すると、リクエストは1回だけ行われ、返されたデータはキャッシュされ、コンポーネント間で共有されます。すべてのコンポーネントはサーバーレンダー全体で同じデータのスナップショットを参照します。
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);
// ...
}AnimatedWeatherCard と MinimalWeatherCard の両方が同じ city でレンダーする場合、それらは メモ化された関数 から同じデータのスナップショットを受け取ります。
AnimatedWeatherCard と MinimalWeatherCard が異なる city 引数を getTemperature に供給する場合、fetchTemperature は2回呼び出され、各呼び出しサイトは異なるデータを受け取ります。
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 せず、2回目 は await します。await は JavaScript の演算子で、Promise の結果を待って返します。最初の getData 呼び出しは単に fetch を開始して Promise をキャッシュし、2回目の getData がルックアップします。
2回目の呼び出し までに Promise がまだ pending の場合、await は結果を待ちます。最適化は、fetch を待っている間に React が計算作業を続けることができるため、2回目の呼び出し の待ち時間を短縮することです。
Promise がすでに解決している場合、エラーまたは fulfilled の結果になると、await はその値をすぐに返します。どちらの結果でも、パフォーマンスの利点があります。
さらに深く知る
すべての言及された 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 は同じレコードで 2 つの 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 を使用して書き直すと、この場合 2 番目の 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 に基づいてコンポーネントのレンダリングをメモ化します。これは特定の計算に対してではなく、メモ化されたコンポーネントは最後のレンダリングと最後の prop 値のみをキャッシュします。一度 props が変更されると、キャッシュは無効化され、コンポーネントは再レンダリングされます。
トラブルシューティング
メモ化された関数が、同じ引数で呼び出されても実行される
以前に述べた落とし穴を参照してください。
上記のいずれも該当しない場合、Reactがキャッシュ内に何かが存在するかどうかを確認する方法に問題があるかもしれません。
引数がプリミティブ(例:オブジェクト、関数、配列)でない場合、同じオブジェクト参照を渡していることを確認してください。
メモ化関数を呼び出すとき、Reactは入力引数を調べて結果がすでにキャッシュされているかどうかを確認します。Reactは引数の浅い等価性を使用してキャッシュヒットがあるかどうかを判断します。
import {cache} from 'react';
const calculateNorm = cache((vector) => {
// ...
});
function MapMarker(props) {
// 🚩 間違い:propsは毎回のレンダーで変わるオブジェクトです。
const length = calculateNorm(props);
// ...
}
function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}この場合、2つの MapMarker は同じ作業を行い、calculateNorm を {x: 10, y: 10, z:10} の同じ値で呼び出しているように見えます。オブジェクトが同じ値を含んでいても、それぞれのコンポーネントが自身の props オブジェクトを作成するため、同じオブジェクト参照ではありません。
Reactは入力に対して Object.is を呼び出し、キャッシュヒットがあるかどうかを確認します。
import {cache} from 'react';
const calculateNorm = cache((x, y, z) => {
// ...
});
function MapMarker(props) {
// ✅ 良い:プリミティブをメモ化関数に渡す
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 に渡すことです。これは次元自体がプリミティブであるため、機能します。
別の解決策は、ベクトルオブジェクト自体をコンポーネントのpropsとして渡すことかもしれません。同じオブジェクトを両方のコンポーネントインスタンスに渡す必要があります。
import {cache} from 'react';
const calculateNorm = cache((vector) => {
// ...
});
function MapMarker(props) {
// ✅ 良い:同じ `vector` オブジェクトを渡す
const length = calculateNorm(props.vector);
// ...
}
function App() {
const vector = [10, 10, 10];
return (
<>
<MapMarker vector={vector} />
<MapMarker vector={vector} />
</>
);
}