はじめに
React(v16.12.0)のReact.memo
、useCallback
、useMemo
の基本的な使い方、使い所に関しての備忘録です。
- 「React でのパフォーマンス最適化の手段を知りたい」
- 「なぜ
React.memo
、useCallback
、useMemo
を利用するのかわからない」
といった人達向けに書いた記事です。
デモは CodeSandbox 上に置いてあります。編集して動作を確認してみると理解が深まると思います。
本記事で用いている用語
- メモ化
- 計算結果
メモ化
計算結果を保持し、それを再利用する手法のこと。
キャッシュのようなものだとイメージすれば良いと思う。
そのため、以下の言葉の意味は大体同じ。
- 「メモ化された値」=「計算結果が保持された値」
- 「メモ化する」=「計算結果を再利用できるように保持する」
メモ化によって都度計算する必要がなくなるため、パフォーマンスの向上が期待できる。
計算結果
以下のような計算の結果のこと。
// result は 1 + 2 の計算結果を格納している変数
const result = 1 + 2;
// result2 は [1, 2, 3, 4, 5].map(number => number * 2) の計算結果を格納している変数
const result2 = [1, 2, 3, 4, 5].map(number => number * 2);
// result3 は React.createElement("div", null, `Hello ${this.props.name}`) の計算結果を格納している変数
const result3 = React.createElement("div", null, `Hello ${this.props.name}`);
React におけるパフォーマンス最適化
React では、不要な再計算やコンポーネントの再レンダリングを抑えることが、パフォーマンス最適化の基本的な戦略となる。
それらを実現する手段としてReact.memo
、useCallback
、useMemo
を利用する。
React 以外のパフォーマンスチューニングにも言えることだが、計測は必須。
無闇に利用してもパフォーマンスが向上するわけではなく、意味がない場合もあるため注意。
React.memo
コンポーネント(コンポーネントのレンダリング結果)をメモ化する React の API(メソッド)。
コンポーネントをメモ化することで、コンポーネントの再レンダリングをスキップできる。
なぜ React.memo を利用するのか
以下のようなコンポーネントの再レンダリングをスキップすることで、パフォーマンスの向上が期待できるから。
- レンダリングコストが高いコンポーネント
- 頻繁に再レンダリングされるコンポーネント内の子コンポーネント
通常のコンポーネントに対しては、わざわざReact.memo
を利用する必要はない。
React.memo の構文
React.memo(コンポーネント);
例えば、Hello
というコンポーネントをメモ化する場合は以下のようになる。
const Hello = React.memo(props => {
return <h1>Hello {props.name}</h1>;
});
React.memo
は Props の等価性(値が等価であること)をチェックして再レンダリングの判断をする。
新しく渡された Props と前回の Props を比較し、等価であれば再レンダリングをせずにメモ化したコンポーネントを再利用する。
そのため、上記のHello
コンポーネントの場合、props.name
が更新されない限りコンポーネントは再レンダリングされない。
React.memo の利用例
React.memo
を利用する場合と、しない場合では何が違うのか比較してみる。
React.memo を利用しない場合
通常、コンポーネントの state が更新されると、そのコンポーネントは再レンダリングされる。
以下のデモのように親コンポーネントが再レンダリングされると、その子コンポーネントも常に再レンダリングされる。
デモを見る
import React, { useState } from "react";
const Child = props => {
console.log("render Child");
return <p>Child: {props.count}</p>;
};
export default function App() {
console.log("render App");
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<>
<button onClick={() => setCount1(count1 + 1)}>countup App count</button>
<button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
<p>App: {count1}</p>
<Child count={count2} />
</>
);
}
これが通常の挙動なので、この書き方が悪いわけではなく、問題もない。
コンポーネントの不要な再レンダリングでパフォーマンスの問題が発生した場合、React.memo
の利用を検討する。
今回はChild
コンポーネントが常に再レンダリングされても何も問題はないため、React.memo
を利用する必要はない。
React.memo を利用する場合
以下はReact.memo
を利用し、Child
コンポーネントの再レンダリングをスキップしているデモ。
デモを見る
import React, { useState } from "react";
const Child = React.memo(props => {
console.log("render Child");
return <p>Child: {props.count}</p>;
});
export default function App() {
console.log("render App");
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<>
<button onClick={() => setCount1(count1 + 1)}>countup App count</button>
<button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
<p>App: {count1}</p>
<Child count={count2} />
</>
);
}
count1
を更新してApp
コンポーネントを再レンダリングした時は、Child
コンポーネントに渡される Props(count2
)は更新されないため、再レンダリングはスキップされる。
Child
コンポーネントに渡されるcount2
が更新された時だけ、再レンダリングされるようになった。
レンダリングコストが高いコンポーネントをメモ化する
極端な例になるが、以下のデモのようにレンダリングコストが高いコンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見る
import React, { useState } from "react";
const Child = React.memo(props => {
let i = 0;
while (i < 1000000000) i++;
console.log("render Child");
return <p>Child: {props.count}</p>;
});
export default function App() {
console.log("render App");
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<>
<button onClick={() => setCount1(count1 + 1)}>countup App count</button>
<button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
<p>App: {count1}</p>
<Child count={count2} />
</>
);
}
頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化する
以下のデモのように、頻繁に再レンダリングされるコンポーネント内の子コンポーネントをメモ化することで、パフォーマンスの向上が期待できる。
デモを見る
import React, { useState, useEffect, useRef } from "react";
const Child = React.memo(() => {
console.log("render Child");
return <p>Child</p>;
});
export default function App() {
console.log("render App");
const [timeLeft, setTimeLeft] = useState(100);
const timerRef = useRef(null);
const timeLeftRef = useRef(timeLeft);
useEffect(() => {
timeLeftRef.current = timeLeft;
}, [timeLeft]);
const tick = () => {
if (timeLeftRef.current === 0) {
clearInterval(timerRef.current);
return;
}
setTimeLeft(prevTime => prevTime - 1);
};
const start = () => {
timerRef.current = setInterval(tick, 10);
};
const reset = () => {
clearInterval(timerRef.current);
setTimeLeft(100);
};
return (
<>
<button onClick={start}>start</button>
<button onClick={reset}>reset</button>
<p>App: {timeLeft}</p>
<Child />
</>
);
}
コールバック関数を Props として受け取ったコンポーネントは必ず再レンダリングされる
以下のデモのようにコールバック関数を受け取ったコンポーネントはReact.memo
を利用しても必ず再レンダリングされる。
デモを見る
import React, { useState } from "react";
const Child = React.memo(props => {
console.log("render Child");
return <button onClick={props.handleClick}>Child</button>;
});
export default function App() {
console.log("render App");
const [count, setCount] = useState(0);
// 関数はコンポーネントが再レンダリングされる度に再生成されるため、
// 関数の内容が同じでも、新しい handleClick と前回の handleClick は
// 異なるオブジェクトなので、等価ではない。
// そのため、コンポーネントが再レンダリングされる。
const handleClick = () => {
console.log("click");
};
return (
<>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment count</button>
<Child handleClick={handleClick} />
</>
);
}
以下のように参照が異なる関数は別のオブジェクトとなる。
function doSomething() {
console.log("doSomething");
}
const func1 = doSomething;
const func2 = doSomething;
console.log(doSomething === doSomething); // true
console.log(func1 === func2); // true
const func3 = () => {
console.log("doSomething");
};
const func4 = () => {
console.log("doSomething");
};
console.log(func3 === func4); // false
前述のhandleClick
が参照する関数も、App
コンポーネントが再レンダリングされる度に再生成されるため、等価ではない。
そのため、関数の内容が同じでもChild
コンポーネントが再レンダリングされる。
この問題を解消するためには、useCallback
を利用して関数をメモ化する必要がある。
useCallback
メモ化されたコールバック関数を返すフック。
なぜ useCallback を利用するのか
React.memo
と併用することで、コンポーネントの不要な再レンダリングをスキップできるから。
より具体的に言えば、React.memo
でメモ化したコンポーネントにuseCallback
でメモ化したコールバック関数を Props として渡すことで、コンポーネントの不要な再レンダリングをスキップできるから。
useCallback の構文
useCallback(コールバック関数, 依存配列);
依存配列とは、コールバック関数が依存している要素が格納された配列のこと。
例えば、count
という変数をconsole.log
で出力する関数をメモ化したい場合は以下のようになる。
const callback = useCallback(() => console.log(count), [count]);
依存している要素が更新されれば、関数が再生成される。
依存している要素がなければ、依存配列は空で OK。
const callback = useCallback(() => console.log("doSomething"), []);
useCallback の利用例
以下はメモ化したコールバック関数を渡し、コンポーネントは再レンダリングをスキップしているデモ。
デモを見る
import React, { useState, useCallback } from "react";
const Child = React.memo(props => {
console.log("render Child");
return <button onClick={props.handleClick}>Child</button>;
});
export default function App() {
console.log("render App");
const [count, setCount] = useState(0);
// 関数をメモ化すれば、新しい handleClick と前回の handleClick は
// 等価になる。そのため、Child コンポーネントは再レンダリングされない。
const handleClick = useCallback(() => {
console.log("click");
}, []);
return (
<>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment count</button>
<Child handleClick={handleClick} />
</>
);
}
useCallback の注意点
前述の通り、useCallback
はReact.memo
と併用するものなので、以下のような使い方をしても意味がない(コンポーネントの不要な再レンダリングをスキップできない)ので注意。
-
React.memo
でメモ化をしていないコンポーネントにuseCallback
でメモ化をしたコールバック関数を渡す -
useCallback
でメモ化したコールバック関数を、それを生成したコンポーネント自身で利用する
React.memo でメモ化をしていないコンポーネントに useCallback でメモ化をしたコールバック関数を渡す
以下のように、メモ化をしていないコンポーネントにメモ化をしたコールバック関数を渡しても、コンポーネントは常に再レンダリングされてしまう。
import React, { useState, useCallback } from "react";
// React.memo でメモ化をしていないコンポーネントのため、メモ化されたコールバック関数を渡されても意味がない。
// App コンポーネントがレンダリングされる度に再レンダリングされる。
const Child = props => {
console.log("render Child");
return <button onClick={props.handleClick}>Child</button>;
};
export default function App() {
console.log("render App");
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("click");
}, []);
return (
<>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment count</button>
<Child handleClick={handleClick} />
</>
);
}
useCallback でメモ化したコールバック関数を、それを生成したコンポーネント自身で利用する
以下の例では、メモ化したコールバック関数をApp
コンポーネント自身で利用している。
動作はするが、「コンポーネントの再レンダリングをスキップする」という目的を達成できてない。
import React, { useState, useCallback } from "react";
export default function App() {
console.log("render App");
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("memonized callback");
}, []);
return (
<>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment count</button>
<button onClick={handleClick}>logging</button>
</>
);
}
useMemo
メモ化された値を返すフック。
コンポーネントの再レンダリング時に値を再利用できる。
なぜ useMemo を利用するのか
値の不要な再計算をスキップすることで、パフォーマンスの向上が期待できるから。
useMemo の構文
useMemo(() => 値を計算するロジック, 依存配列);
依存配列とは、値を計算するロジックが依存している要素(値の計算に必要な要素)が格納された配列のこと。
例えば、count
という変数の値を2倍にした値をメモ化したい場合は以下のようになる。
const result = useMemo(() => count * 2, [count]);
依存している要素が更新されれば、値が再計算される。
useMemo の利用例
useMemo
を利用する場合と、しない場合では何が違うのか比較してみる。
useMemo を利用しない場合
以下はuseMemo
を利用せず、不要な再計算が発生しているデモ
デモを見る
import React, { useState } from "react";
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
// 引数の数値を2倍にして返す。
// 不要なループを実行しているため計算にかなりの時間がかかる。
const double = count => {
let i = 0;
while (i < 1000000000) i++;
return count * 2;
};
// count2 を2倍にした値
// double(count2) はコンポーネントが再レンダリングされる度に実行されるため、
// count1 を更新してコンポーネントが再レンダリングされた時にも実行されてしまう。
// そのため、count1 を更新してコンポーネントを再レンダリングする時も時間がかかる。
// count1 を更新しても doubledCount の値は変わらないため、count1 を更新した時に
// double(count2) を実行する意味がない。したがって、不要な再計算が発生している状態である。
// count1 が更新されてコンポーネントが再レンダリングされた時は double(count2) が実行されないようにしたい。
const doubledCount = double(count2);
return (
<>
<h2>Increment count1</h2>
<p>Counter: {count1}</p>
<button onClick={() => setCount1(count1 + 1)}>Increment count1</button>
<h2>Increment count2</h2>
<p>
Counter: {count2}, {doubledCount}
</p>
<button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
</>
);
}
count1
を更新した時もdouble(count2)
が実行されてしまうため、count1
を更新してコンポーネントを再レンダリングする時も時間がかかる。
useMemo を利用する場合
以下はuseMemo
を利用し、不要な再計算をスキップするデモ。
デモを見る
import React, { useState, useMemo } from "react";
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
// 引数の数値を2倍にして返す。
// 不要なループを実行しているため計算にかなりの時間がかかる。
const double = count => {
let i = 0;
while (i < 1000000000) i++;
return count * 2;
};
// count2 を2倍にした値をメモ化する。
// 第2引数に count2 を渡しているため、count2 が更新された時だけ値が再計算される。
// count1 が更新され、コンポーネントが再レンダリングされた時はメモ化した値を利用するため再計算されない。
const doubledCount = useMemo(() => double(count2), [count2]);
return (
<>
<h2>Increment(fast)</h2>
<p>Counter: {count1}</p>
<button onClick={() => setCount1(count1 + 1)}>Increment(fast)</button>
<h2>Increment(slow)</h2>
<p>
Counter: {count2}, {doubledCount}
</p>
<button onClick={() => setCount2(count2 + 1)}>Increment(slow)</button>
</>
);
}
useMemo
を利用して値をメモ化したため、count1
を更新した時はdouble(count2)
が実行されないようになった。
そのため、count1
を更新した時のコンポーネントの再レンダリングが高速になった。
コンポーネントの再レンダリングをスキップする
useMemo
はレンダリング結果もメモ化できるため、React.memo
のようにコンポーネントの再レンダリングをスキップできる。
以下はコンポーネントをメモ化して、不要な再レンダリングをスキップしているデモ。
デモを見る
import React, { useState, useMemo } from "react";
export default function App() {
console.log("render App");
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
// 引数の数値を2倍にして返す。
// 無駄なループを実行しているため計算にかなりの時間がかかる。
const double = count => {
let i = 0;
while (i < 1000000000) i++;
return count * 2;
};
// レンダリング結果(計算結果)をメモ化する
// 第2引数に count2 を渡しているため、count2 が更新された時だけ再レンダリングされる。
// count1 が更新され、コンポーネントが再レンダリングされた時はメモ化したレンダリング結果を
// 利用するため再レンダリングされない。
const Counter = useMemo(() => {
console.log("render Counter");
const doubledCount = double(count2);
return (
<p>
Counter: {count2}, {doubledCount}
</p>
);
}, [count2]);
return (
<>
<h2>Increment count1</h2>
<p>Counter: {count1}</p>
<button onClick={() => setCount1(count1 + 1)}>Increment count1</button>
<h2>Increment count2</h2>
{Counter}
<button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
</>
);
}
関数コンポーネント内でコンポーネントをメモ化したい場合はuseMemo
を利用する。
以下のデモのように関数コンポーネント内でReact.memo
を利用しても意味がないので注意。
デモを見る
import React, { useState } from "react";
export default function App() {
console.log("render App");
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
// 引数の数値を2倍にして返す。
// 無駄なループを実行しているため計算にかなりの時間がかかる。
const double = count => {
let i = 0;
while (i < 1000000000) i++;
return count * 2;
};
// App コンポーネントが再レンダリングされたら
// このコンポーネントも必ず再レンダリングされる
const Counter = React.memo(props => {
console.log("render Counter");
const doubledCount = double(props.count2);
return (
<p>
Counter: {props.count2}, {doubledCount}
</p>
);
});
return (
<>
<h2>Increment count1</h2>
<p>Counter: {count1}</p>
<button onClick={() => setCount1(count1 + 1)}>Increment count1</button>
<h2>Increment count2</h2>
<Counter count2={count2} />
<button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
</>
);
}
useCallback を関数の再生成を防ぐ目的で利用してはいけないのか?
「useMemo
が再計算を防ぐために利用するのであれば、useCallback
も関数の再生成を防ぐために利用するのは意味があるのでは?」と思った方もいると思います。
しかし、私はそれを目的として利用することは理にかなっていないと思っています。
なぜなら、「関数の再生成するコスト > useCallback
の実行コスト」になることはないと認識しているからです。
useCallback
とuseMemo
のどちらも、メモ化をする処理自体にコストがあります。
useMemo の場合
useMemo
の場合、「再計算のコスト < `useMemo`の実行コスト」の時もあれば、「再計算のコスト > useMemo
の実行コスト」の時もあると認識しています。
極端な例になりますが、以下は「再計算のコスト < useMemo
の実行コスト」の例です。
const result = useMemo(() => value * 2, [value]);
value * 2
は単純な計算なので、useMemo
を利用しても効果はありません。
寧ろ、useMemo
の実行コストの方が高いかもしれないため、上記のようなシーンでuseMemo
を利用するのは理にかなっていないと思っています。
そして、以下は「再計算のコスト > useMemo
の実行コスト」の例です。
const result = useMemo(() => {
let i = 0;
while (i < 1000000000) i++;
count * 2;
}, [value]);
この場合、明らかに再計算のコストの方が高いため、useMemo
を利用すると大きな効果が得られます。
useCallback の場合
以下は「関数の再生成するコスト < useCallback
の実行コスト」の例です。
const handleClick = useCallback(() => {
console.log(value);
}, [value]);
前述のuseMemo
と同様で、わざわざ利用する必要がないと思っています。
そして、「関数の再生成するコスト > useCallback
の実行コスト」ですが、この状況が思いつかないです。
// ?
なので、関数の再生成を防ぐためにuseCallback
を利用するの理にかなってないし、わざわざ利用する必要はないと思っています。
厳密な測定をしたわけではないので、この認識は間違っているかもしれないです。
もし、私の認識が間違っている(「関数の再生成するコスト > useCallback
の実行コスト」になる状況がある、もしくは他に使い道がある)場合、お手数ですが具体例(コード)を添えて、ご意見ご指摘いただけると大変助かります。
依存配列は正しく指定する必要がある
useCallback
とuseMemo
の依存配列は正しく指定しないとバグの原因になる。
そのため、以下のコードは NG。
// 依存要素である count2 が依存配列にないため NG
const result = useMemo(() => count * count2, [count]);
// これが正しい
// const result = useMemo(() => count * count2, [count, count2]);
そのため、eslint-plugin-react-hooksなどを利用して、必ず Lint する。
使い所
パフォーマンスを計測し、ボトルネックになっている箇所に適用していくのが一番効果的。
とは言え、GitHub 上にあるコードや技術書を見てみると、ガンガン利用していた。
厳密な利用基準を設けるのも大変なので、それぞれの機能や役割をちゃんと理解しているのであれば、積極的に利用しても大きな問題はないと思った。
終わり
今回準備したデモは極端な例ですが、利用シーンによっては非常に有用な機能です。
状況に応じて利用していきましょう。
本記事以外にも React に関連する記事を書いておりますので、興味があればそちらもどうぞ。
お知らせ
Udemy で webpack の講座を公開したり、Kindle で技術書を出版しています。
Udemy:
webpack 最速入門(10,800 円 -> 2,000 円)
Kindle(Kindle Unlimited だったら無料):
React Hooks 入門(500 円)
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。