【React】メモ化を行う基準
こんにちは、株式会社Gizumoでエンジニアをしている田中です。
React開発を行う上で、メモ化を行う基準が分からないという方が多いのではないでしょうか。私も何度も調べては答えが出ず、結局は「とりあえずメモ化しておけば良いんじゃない?」という感じでした。
しかし、2023年3月に正式リリースされた新たなReactの公式ドキュメントでメモ化に関する詳細がかなりアップデートされておりとても参考になるため、今回はその内容を元にメモ化を行う基準についてまとめていきます。
目次
TL;DR
- メモ化を効率的に行いたい場合、「必要なタイミングでのみメモ化を行い、それ以外ではメモ化しない」
- メモ化をしたいが効率的に行うためのリソースを割けない場合、「個々のケースについて考えず、できるだけメモ化することを選択する」
前提・対象とする読者
- サンプルコードは
React
で記述
また、対象とする読者は以下のような方を想定しています。
React
で開発を行っている方- メモ化を行う基準について知りたい方
メモ化とは
メモ化とは、変数・関数・コンポーネントのキャッシュを再利用しコンポーネントの不要な再レンダリングを抑えることです。目的はアプリケーションパフォーマンスの最適化です。
メモ化には、memo
、useMemo
、useCallback
というReact
のAPI
やhooks
が使われます。それぞれの使用に必要なタイミングについて見ていきましょう。
参考
memo
memo
は、コンポーネントをメモ化するための高階コンポーネント(Higher-Order Component
)です。memo
を使用すると、コンポーネントの再レンダリングを最適化できます。コンポーネントの再レンダリングは、コンポーネントへのprops
が変更された場合に発生します。そのため、memo
は以下のような場合に使用することが推奨されています。
memo
が必要なタイミング
memo
が必要なタイミングは、以下のような場合です。
- コンポーネントが
props
の変更によって再レンダリングされる可能性が低い場合 - コンポーネント内での処理が重く、その処理が同じ結果を返す場合
コンポーネントがprops
の変更によって再レンダリングされる可能性が低い場合
通常、親コンポーネントから渡されるprops
が変更されるか親コンポーネントが再レンダリングされると、子コンポーネントは再レンダリングされます。しかし、props
が頻繁に変更されない場合、そのコンポーネントをmemo
でラップすることで、親コンポーネントの再レンダリングによる子コンポーネントの再レンダリングを無くすことができます。それによって子コンポーネントの再レンダリング条件をprops
の変更のみに絞ることができます。よってコンポーネントがprops
の変更によって再レンダリングされる可能性が低い場合、再レンダリングの回数をかなり減らすことができます。
コンポーネント内での処理が重く、その処理が同じ結果を返す場合
もしコンポーネント内で重い計算や処理が行われており、その結果が常に同じである場合、memo
を使用することでキャッシュを利用できます。memo
は、コンポーネントの入力が変更されない限り、以前の結果を再利用します。これにより、再計算を避けることができます。とくに、計算量が多い場合や、外部のリソースへのアクセスが必要な場合には、memo
を使用して結果をキャッシュすることで効率化が図れます。
import { memo } from 'react';
export const HeavyCalculationComponent = memo((props) => {
// 重い計算や処理を行う関数
const performHeavyCalculation = (value) => {
// 重い計算や処理の実装
// 例えば、非常に時間のかかるループ処理や再帰処理などを想定
let result = 0;
for (let i = 0; i < value; i++) {
result += i;
}
return result;
};
// propsが変更されない限り、同じ結果を返す(useMemoについては後述)
const sameResultEveryTime = performHeavyCalculation(props.value)
return (
<div>
<p>入力値: {props.value}</p>
<p>計算結果: {sameResultEveryTime}</p>
</div>
);
});
参考:Should you add memo everywhere?
useMemo
useMemo
は、指定した関数の結果をメモ化するためのフックです。関数の実行結果をキャッシュし、依存する値が変化しない限り、キャッシュされた値を再利用します。これにより、重い計算や処理を最適化できます。そのため、useMemo
は以下のような場合に使用することが推奨されています。
useMemo
が必要なタイミング
useMemo
が必要なタイミングは、以下のような場合です。
- 関数内での計算や処理が重く、依存関係がほとんど変化しない場合
- 計算結果を再利用する必要がある場合
関数内での計算や処理が重く、依存関係がほとんど変化しない場合
このような場合、同じ入力に対して同じ結果を返す関数を再評価する必要がありません。useMemo
を使うことで、関数の再評価を最適化し、不要な計算を避けることができます。
以下は、このようなケースでのuseMemo
の使用例です。
import { useMemo } from 'react';
export const HeavyCalculationComponent = (props) => {
const performHeavyCalculation = (value) => {
// 重い計算や処理の実装
// 例えば、非常に時間のかかるループ処理や再帰処理などを想定
let result = 0;
for (let i = 0; i < value; i++) {
result += i;
}
return result;
};
// propsが変更されない限り、同じ結果を返す
const memoizedResult = useMemo(() => performHeavyCalculation(props.value), [props.value]);
return (
// JSXを返す
);
};
計算結果を他のコンポーネントや処理で再利用する場合
これにより、同じ結果を持つコンポーネントや処理間で計算を再実行する必要がなくなり、パフォーマンスが向上します。
import { useMemo } from 'react';
type Props = {
data: number[];
}
export const CalculationComponent = ({ data }: Props) => {
const memoizedResult = useMemo(() => {
// 計算や処理を行う
// 例えば、dataの要素の合計を計算する処理などを想定
let sum = 0;
for (let i = 0; i < data.length; i++) {
sum += data[i];
}
return sum;
}, [data]);
useEffect(() => {
// memoizedResultを使用した処理
}, [memoizedResult])
return (
// JSXを返す
);
};
参考:Should you add useMemo everywhere?
useCallback
useCallback
は、指定した関数をメモ化するためのフックです。コールバック関数をキャッシュし、依存する値が変化しない限り、同じコールバック関数のインスタンスを再利用します。そのため、useCallback
は以下のような場合に使用することが推奨されています。
useCallback
が必要なタイミング
useCallback
が必要なタイミングは、以下のような場合です。
- 関数をメモ化されたコンポーネントに
props
として渡す場合 - 関数が何らかの
hooks
の依存関係として使用される場合
関数をメモ化されたコンポーネントにprops
として渡す場合
子コンポーネントにprops
として関数を渡す必要があるとき、その関数がメモ化されていれば、その関数の再生成回数は少なくなるため子コンポーネントの再レンダリングを抑えることができます。(そのメモ化された関数が持つ依存関係の変更回数に限定できる。)
- Propsとして渡す関数をメモ化していない時、親コンポーネントのレンダリング時に毎回レンダリングされている(6回のカウントアップに合わせて
CounterComponent
が6回レンダリングされている)
- Propsとして渡す関数をメモ化している時、親コンポーネントのレンダリング時にレンダリングされない(関数の依存関係変更がないため関数の再生成が起きず、それに合わせて
CounterComponent
のレンダリングが抑えられている)
- ソースコード
// 子コンポーネント
import { memo } from "react";
type Props = {
counter: () => void;
}
export const CounterComponent = memo(({ counter }: Props) => {
console.log('render CounterComponent');
return (
<div>
<h2 style={{ color: 'blue'}}>Counter Component</h2>
<button onClick={counter}>count up in CounterComponent</button>
</div>
)
})
// 親コンポーネント
import { useCallback, useState } from 'react'
import './App.css'
import { CounterComponent } from './CounterComponent';
function App() {
const [count, setCount] = useState(0)
const [countV2, setCountV2] = useState(0)
// Propsとして渡す関数
const counter = useCallback(() => {
setCount((prev) => prev + 1);
}, [setCount]);
const counterV2 = useCallback(() => {
setCountV2((prev) => prev + 1);
}, [setCountV2]);
return (
<>
<p>{count}</p>
<button onClick={counter}>count up</button>
<p>{countV2}</p>
<button onClick={counterV2}>count up V2</button>
<br />
<CounterComponent counter={counter} />
</>
)
}
export default App
関数が何らかのhooks
の依存関係として使用される場合
useCallback
は、他のHooks
の依存関係として使用される場合にも有用です。たとえば、useEffect
やuseMemo
などのHooks
では、特定の関数が変更された場合に処理を実行したり、値を再計算したりすることがあります。その際、依存関係の関数が頻繁に変更されない場合、useCallback
を使用して関数をキャッシュし、不要な処理や再計算を防ぐことができます。
import { useState, useEffect, useCallback } from 'react';
export const CounterComponent = () => {
const [count, setCount] = useState(0);
// カウントアップのコールバック関数をメモ化
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, [setCount]);
// increment関数に依存して副作用を実行
useEffect(() => {
console.log('Increment function changed');
// 他の処理や副作用を実行
// ...
}, [increment]);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
参考:Should you add useCallback everywhere?
すべてのhooks
に共通していること
すべてのhooks
で共通して以下のようなことが書かれています。
There is no benefit to wrapping a function in {hook} in other cases. There is no significant harm to doing that either, so some teams choose to not think about individual cases, and memoize as much as possible. The downside is that code becomes less readable. Also, not all memoization is effective: a single value that’s “always new” is enough to break memoization for an entire component.
上記を訳すると以下になります。
- それぞれの
hooks
の必要なタイミング以外ではメモ化をするメリットはない - しかしメモ化することに大きな害もない
- チームによっては個々のケースについて考えず、できるだけメモ化することを選択する
- メモ化するデメリットは、コードが読みづらくなること
- また、すべてのメモ化が有効なわけではない。”常に新しい “1つの値だけで、コンポーネント全体のメモ化が壊れてしまう
参考
Should you add memo everywhere?
Should you add useMemo everywhere?
Should you add useCallback everywhere?
まとめ
これまでのことからメモ化の基準を結論づけると、「必要なタイミングでのみメモ化を行い、それ以外ではメモ化しない」となります。
その基準って難しくない?
しかしこの基準を設けて実際にプロジェクトに導入するのって難しくないでしょうか?実装者全員がそれぞれのhooks
に必要なタイミングを判断し適切にメモ化を行いながら実装をするというのは無理があるように思えます。
そういう背景からメモ化の基準は公式に倣い「個々のケースについて考えず、できるだけメモ化することを選択する」とするのは1つの手ではないかと思われます。