本記事の目的
React.memo
useMemo
useCallback
の基本的な使い方や具体的な利用用途を調査した内容を備忘録として記録する。
- Reactを用いたシステムのパフォーマンス最適化の手段を知りたい
- useMemoとか聞いたことはあるけど、どこに使うべきかわからない
- これらのメソッドを使ったことがない
上記のような方々が参考になるような記事を目指します。
パフォーマンス最適化とは
一般的に不要な処理を削減したり、レンダリングとは関係のない処理を遅延させたりすることでUXを向上させることを目的に行われます。
Reactにおいては計算結果が前回の結果と等価となるような不要な再計算や不要なコンポーネントの再レンダリングを防ぐことがパフォーマンス最適化の第一歩となり得ます。
Reactではパフォーマンスを最適化する手段として下記のメソッドが用意されています。
- React.memo
- useMemo
- useCallback
パフォーマンス最適化においては「とりあえずこれをやっておけばいいのね」という精神は捨てて、パフォーマンスの向上が見込めるかを判断しながら実施していく必要があります。
上記の関数も意味のない箇所で使うとパフォーマンスが向上しないどころか、逆にパフォーマンスが下がる結果に繋がる可能性もあります。
React.memo
コンポーネントをメモ化するReactが提供するメソッドです。
コンポーネントをメモ化することでコンポーネントの不要な再レンダリングを防ぐことができます。
メモ化(英: Memoization)とは、プログラムの高速化のための最適化技法の一種であり、サブルーチン呼び出しの結果を後で再利用するために保持し、そのサブルーチン(関数)の呼び出し毎の再計算を防ぐ手法である。
引用元:Wikipedia
メモ化とは計算やレンダリング結果を保持して、その結果を再利用する手法です。
キャッシュはデータを保持して再利用しますが、同じようなイメージでメモ化では計算結果やレンダリング結果を保持して再利用します。
React.memo によるパフォーマンス最適化
以下のようなコンポーネントの不要な再レンダリングを防ぐことで、パフォーマンスの向上を期待できます。
- レンダリングまでの処理コストが高いコンポーネント
- 頻繁に再レンダリングされるコンポーネントの子コンポーネント
上記に該当しないようなコンポーネントに関してはReact.memo
を利用するメリットが低い可能性があります。
React.memo の使い方
基本構文は以下の通りです。
React.memo(コンポーネント);
例えば、Greet(挨拶)
コンポーネントをメモ化する場合は以下のように記述します。
type Props = {
text: string;
};
const Greet: React.FC<Props> = ({ text }) => {
return <h1>{`Hello. ${text}`}</h1>;
};
const memorizedGreet = React.memo(Greet);
React.memo
は引数の等価ではないと判定した場合にのみ再レンダリングを実行させます。反対に等価と判定した場合は再レンダリングを実行せずにメモ化したコンポーネントを再利用します。
上記のGreet
コンポーネントでは、引数のtextが更新されるまでは再レンダリングが実行されません。
具体的な利用例
React.memo未使用
※本記事のコンポーネントではコンソールに値が出力されている=再レンダリングが実行されている証明として説明します。
import { Button } from '@material-ui/core';
import { FC, memo, useState } from 'react';
type Props = {
count: number;
};
const Child: React.FC<Props> = ({ count }) => {
console.log('render child');
return <p>{`Child: ${count}`}</p>;
};
const Index: FC = () => {
console.log('render parent');
const [parentCount, setParentCount] = useState(0);
const [childCount, setChildCount] = useState(0);
return (
<div style={{ margin: '1rem' }}>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black' }}>React.memo未使用</h1>
<p>{`Parent: ${count1}`}</p>
<Child count={count2} />
<Button variant="contained" color="default" onClick={() => setParentCount(parentCount + 1)}>
Parent Count Up
</Button>
<Button variant="contained" color="primary" onClick={() => setChildCount(childCount + 1)}>
Child Count Up
</Button>
</div>
);
};
export default Index;
この事例ではChildコンポーネント(以下、子コンポーネントと呼ぶ)のレンダリングには関係のないparentCountが更新された場合も子コンポーネントが再レンダリングされていることがわかります。
React.memo使用
import { Button } from '@material-ui/core';
import { FC, memo, useState } from 'react';
type Props = {
count: number;
};
const Child: React.FC<Props> = ({ count }) => {
console.log('render child');
return <p>{`Child: ${count}`}</p>;
};
// Childコンポーネントをメモ化
const MemorizedChild = memo(Child);
const Index: FC = () => {
console.log('render parent');
const [parentCount, setParentCount] = useState(0);
const [childCount, setChildCount] = useState(0);
return (
<div style={{ margin: '1rem' }}>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black' }}>React.memo使用</h1>
<p>{`Parent: ${parentCount}`}</p>
<MemorizedChild count={childCount} />
<Button variant="contained" color="default" onClick={() => setParentCount(parentCount + 1)}>
Parent countup
</Button>
<Button variant="contained" color="primary" onClick={() => setChildCount(childCount + 1)}>
Child countup
</Button>
</div>
);
};
export default Index;
この事例では子コンポーネントのレンダリングには関係のないparentCount
が更新された場合は子コンポーネントの再レンダリングを防いでいます。
そして、子コンポーネントの引数であるchildCount
が更新されると再レンダリングが実行されています。
効果的なメモ化
レンダリングの処理コストが高いコンポーネントのメモ化
import { Button } from '@material-ui/core';
import { FC, memo, useState } from 'react';
type Props = {
count: number;
};
const Child: React.FC<Props> = ({ count }) => {
// 重い処理
let i = 0;
while (i < 1000000000) i++;
console.log('render child');
return <p>{`Child: ${count}`}</p>;
};
const MemorizedChild = memo(Child);
const Index: FC = () => {
console.log('render parent');
const [parentCount, setParentCount] = useState(0);
const [childCount, setChildCount] = useState(0);
return (
<div style={{ margin: '1rem' }}>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black' }}>React.memo使用</h1>
<p>{`Parent: ${parentCount}`}</p>
<MemorizedChild count={childCount} />
<Button variant="contained" color="default" onClick={() => setParentCount(parentCount + 1)}>
Parent countup
</Button>
<Button variant="contained" color="primary" onClick={() => setChildCount(childCount + 1)}>
Child countup
</Button>
</div>
);
};
export default Index;
この事例では重い処理を持つ子コンポーネントをメモ化しているため、子コンポーネントとは関係のないparentCount
が更新された時に重い処理を実行していません。
このように重い処理を持つコンポーネントのメモ化はパフォーマンス最適化に効果的である可能性が高いです。
更新頻度の高いコンポーネントの子コンポーネントのメモ化
import { FC, memo, useEffect, useState } from 'react';
const Child: React.FC = () => {
console.log('render child');
return <p>Child</p>;
};
const MemorizedChild = memo(Child);
const Index: FC = () => {
console.log('render parent');
const [count, setCount] = useState(0);
useEffect(() => {
const countUp = setTimeout(() => {
setCount(count + 1);
}, 100);
return () => clearTimeout(countUp);
}, [count]);
return (
<div style={{ margin: '1rem' }}>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black' }}>React.memo使用</h1>
<h1>{count}</h1>
<MemorizedChild />
</div>
);
};
export default Index;
count
が更新されるごとに親コンポーネントは再レンダリングされていますが、子コンポーネントはメモ化によって再レンダリングを防げています。
このように更新頻度の高い親コンポーネントの子コンポーネントのメモ化はパフォーマンス最適化に効果的である可能性が高いです。
意図しない再レンダリングを引き起こすメモ化
ここまでメモ化によって引数が更新されない限りは再レンダリングを防ぐことができている事例を紹介してきました。
ただし、コールバック関数を引数として子コンポーネントに渡す場合は__必ず再レンダリングされます__。
その理由はコールバック関数が再レンダリングの度に再生成されているから
です。再生成されたコールバック関数は前回生成されたコールバック関数とは異なるオブジェクトなので等価ではないと判定されます
import { Button } from '@material-ui/core';
import { FC, memo, useState } from 'react';
type Props = {
countUp: () => void;
};
const Child: React.FC<Props> = ({ countUp }) => {
console.log('render child');
return (
<Button variant="contained" color="default" onClick={countUp}>
Child Button
</Button>
);
};
const MemorizedChild = memo(Child);
const Index: FC = () => {
console.log('render parent');
const [count, setCount] = useState(0);
const parentClick = () => {
setCount(count + 1);
};
// コンポーネントが再レンダリングされる度に新しいコールバック関数として生成される
const childClick = () => {
console.log('child click');
};
return (
<div style={{ margin: '1rem' }}>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black' }}>React.memo使用</h1>
<p>{`Count: ${count}`}</p>
<Button variant="contained" color="primary" onClick={parentClick}>
Parent Button
</Button>
<MemorizedChild countUp={childClick} />
</div>
);
};
export default Index;
このように子コンポーネントをメモ化しても、親コンポーネントが再レンダリングされる度に子コンポーネントも再レンダリングしてしまっています。
これは前述したように再レンダリングの度に子コンポーネントの引数であるコールバック関数が再生成されていることが原因です。
Reactではこの課題を解決する機能を提供しています。
それがコールバック関数をメモ化するuseCallback
というメソッドです。
useCallback
前述したようにコールバック関数をメモ化するReactが提供するメソッドです。
コールバック関数をメモ化することでコンポーネントの再レンダリングによって不要な再生成を防ぐことができます。
useCallback の使い方
基本構文は以下の通りです。
useCallback(コールバック関数, 依存配列);
例えば、与えた引数をコンソールに出力するGreet(挨拶)
メソッドをメモ化する場合は以下のように記述します。
const Greet = useCallback((text: string) => {
console.log({`Hello. ${text}`})
}, [text]);
このように記述すると、Greet関数はメモ化されます。
コンポーネントが再レンダリングされてもuseCallbackの第二引数に指定されている値が更新されない限りはコールバック関数は更新されません。
また、第二引数はカンマ区切りでいくつでも指定することができます。
具体的な利用例
import { Button } from '@material-ui/core';
import { FC, memo, useCallback, useState } from 'react';
type Props = {
countUp: () => void;
};
const Child: React.FC<Props> = ({ countUp }) => {
console.log('render child');
return (
<Button variant="contained" color="default" onClick={countUp}>
Child Button
</Button>
);
};
const MemorizedChild = memo(Child);
const Index: FC = () => {
console.log('render parent');
const [count, setCount] = useState(0);
const parentClick = () => {
setCount(count + 1);
};
const childClick = useCallback(() => {
console.log('child click');
}, []);
return (
<div style={{ margin: '1rem' }}>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black' }}>React.memo使用</h1>
<p>{`Count: ${count}`}</p>
<Button variant="contained" color="primary" onClick={parentClick}>
Parent Button
</Button>
<MemorizedChild countUp={childClick} />
</div>
);
};
export default Index;
このようにuseCallbackでメモ化したコールバック関数を子コンポーネントに引数として渡すことで前項のような意図しない再レンダリングを防ぐことができます。
ただし、無闇にuseCallbackでコールバック関数をメモ化しても意味がありません。
例えば、__useCallbackでメモ化したコールバック関数をメモ化していない子コンポーネントに渡したり、同一コンポーネント内で利用する__のは効果的ではありません。
useMemo
メモ化された値やレンダリング結果を返却するメソッドです。
コンポーネントの再レンダリング時に時間を要する計算を再実行させたくない場合などに有効です。
また、レンダリング結果をメモ化する場合、React.memo
と同じようにコンポーネントの再レンダリングを防ぐことができます。関数コンポーネント内で関数コンポーネントをメモ化する場合はReact.memo
では意味がないので、useMemo
を利用するようにしましょう。
useCallback の使い方
基本構文は以下の通りです。
useMemo(() => 計算ロジック, 依存配列);
useMemo(関数コンポーネント,依存配列);
例えば、count変数
を2倍した値をメモ化したい場合は以下のように記述します。
const result = useMemo(() => count * 2, [count]);
このように記述すると、計算結果がメモ化されます。
コンポーネントが再レンダリングされても依存配列に指定したcountが更新されない限りは再計算されません。ただし、依存配列を指定しないと再計算されなくなるので注意が必要です。
また、第二引数はカンマ区切りでいくつでも指定することができます。
具体的な利用例
useMemo未使用
import { Button } from '@material-ui/core';
import { FC, useState } from 'react';
const Index: FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClick1 = () => {
setCount1(count1 + 1);
};
const handleClick2 = () => {
setCount2(count2 + 1);
};
const calculate = (count: number) => {
let i = 0;
while (i < 1000000000) i++;
return count * 2;
};
const calculatedCount2 = calculate(count2);
return (
<div style={{ margin: '1rem' }}>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black' }}>Count1</h1>
<p>{count1}</p>
<Button variant="contained" color="primary" onClick={handleClick1}>
Count1 + 1
</Button>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black', marginTop: '1rem' }}>Count2の2倍</h1>
<p>{calculatedCount2}</p>
<Button variant="contained" color="primary" onClick={handleClick2}>
Count2 + 1
</Button>
</div>
);
};
export default Index;
Count1
はCount2の2倍
の計算に関係ないにもかかわらず、Count1
が更新された再レンダリング時にCount2の2倍
の値が再計算されてパフォーマンスが低下しています。
このように計算ロジックが複雑(事例では単に重い処理)な計算結果をメモ化せずに利用すると、同一の結果が得られるとしても再レンダリング時に再計算されてしまいます。
useMemo使用
import { Button } from '@material-ui/core';
import { FC, useMemo, useState } from 'react';
const Index: FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClick1 = () => {
setCount1(count1 + 1);
};
const handleClick2 = () => {
setCount2(count2 + 1);
};
const calculate = (count: number) => {
let i = 0;
while (i < 1000000000) i++;
return count * 2;
};
const calculatedCount2 = useMemo(() => calculate(count2), [count2]);
return (
<div style={{ margin: '1rem' }}>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black' }}>Count1</h1>
<p>{count1}</p>
<Button variant="contained" color="primary" onClick={handleClick1}>
Count1 + 1
</Button>
<h1 style={{ fontSize: '1.2rem', borderBottom: '1px solid black', marginTop: '1rem' }}>Count2の2倍</h1>
<p>{calculatedCount2}</p>
<Button variant="contained" color="primary" onClick={handleClick2}>
Count2 + 1
</Button>
</div>
);
};
export default Index;
このようにcount1
の更新時の再レンダリングではcount2
の計算結果は変更されないため、計算結果をメモ化することで不要な再計算を抑止できています。
処理が少ない計算や関数をメモ化すべきではない理由
これまで紹介してきたReact.memoやuseMemo、useCallbackはメソッドです。
非常に便利なメソッドですが、これらのメソッドも開発者が定義するメソッドと同じように処理を行っています。
このメソッドが処理する時間よりも処理時間が少ない処理の場合はパフォーマンスの低下に繋がる恐れがあります。
これらメソッドの処理に要する時間については言及できるほど分析できていませんが、それなりに重そうな処理に利用すれば大概はパフォーマンスの向上に繋がっています。
ざっくりと言うなら、容易な処理には使わない
という意識を持って開発に着手すれば、特に問題にはならないでしょう。
最後に
普段何気なく使っているuseMemoやuseCallbackについて少しでも理解が深まりましたら幸いです。
ご指摘等あれば、ぜひコメント欄からお願いいたします。