疑問
Reactでパフォーマンス最適化を行う際に「useMemo
」と「useCallback
」というものが登場するが、これらの違いって何...?
結論
Reactでは、「特定の場合にしか必要のない処理なのに、コンポーネントの再レンダリングによって無駄にその処理が再実行されてしまう」ということがたまに発生します。
そこでReactでは、「この条件を満たす場合に だけ 処理を再実行せい」という指定をすることができます。これがいわゆる「メモ化」と呼ばれるものです。
useMemo
とuseCallback
はこの「メモ化」を行うための手段で、「何に対して再実行の条件をつけるか」という点が異なります。
⭐️ useMemo
は、「処理の再計算」に条件をつけることができます。
⭐️ useCallback
は、「関数の再定義」に条件をつけることができます。
詳しく解説
Reactコンポーネントではリアクティブなデータを定義する際にuseState
を使用します。そのデータが更新されると、コンポーネントの再レンダリングが実行されます。
以下のようなコンポーネントを例とすると、テキストフォームの入力値でStateのname
を更新しているので、フォームへの入力がある度にブラウザのコンソールに「再レンダリング」と出力されます。
この挙動は時に画面が重くなる原因となります。
export const HelloWorld = () => {
const [name, setName] = useState<string>(""); // Stateを定義
console.log("再レンダリング"); // nameが更新される度にログが出力される
return (
<div>
<input
type="text"
onChange={(e) => setName(e.target.value)} // 入力値でStateの値nameを更新
value={name}
/>
</div>
);
};
useMemoとは何か / 使い所
以下のようなコンポーネントがあるとします。とても重い処理を内部で行なっている関数 heavyCalc
の結果を doubleCount
という変数に格納しています。
このheavyCalc
関数内で使用している(依存している)Stateの値はcount
だけなので、count
が更新された場合のみ「const doubleCount = heavyCalc();
」が再実行されて欲しいのですが、
先述の例でお話した通り、ReactではStateの値に更新があった場合にはコンポーネント内の処理が再実行される為、フォームに値が入力されてheavyCalc
関数に全く関係のないState name
が更新された場合にも、毎回「const doubleCount = heavyCalc();
」が再実行されてしまいます。
export const HelloWorld = () => {
const [name, setName] = useState<string>("");
const [count, setCount] = useState<number>(0);
const heavyCalc = (): number => {
let x = 0;
for (let i = 0; i < 100000000; i++) {
x++;
}
return count * 2;
};
const doubleCount = heavyCalc(); // レンダリングされる度に実行される
return (
<div>
<input
type="text"
onChange={(e) => setName(e.target.value)}
value={name}
/><br/><br/>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<p>Double count: {doubleCount}</p>
</div>
);
};
その結果、テキストフォームに値を入力するだけでも処理時間が長くなり、かなり画面がもっさりしてしまいます。
そこで使用するのが「useMemo
」です。
useMemo
は、「処理の再計算」に条件をつけることができます。
heavyCalc();
という処理の再計算に条件をつけることでパフォーマンス最適化を図ることができます。
使い方は簡単です。第一引数に「再計算されてほしくない処理」を指定して、第二引数に「コイツが更新されたら再計算していいよ。なヤツ」を指定します。
再計算されてほしくない処理は heavyCalc();
で、その処理が依存する値(コイツが更新されたら再計算していいよ。なヤツ)は count
なので、useMemo(() => heavyCalc(), [count])
のような形になります。
export const HelloWorld = () => {
const [name, setName] = useState<string>("");
const [count, setCount] = useState<number>(0);
const heavyCalc = (): number => {
let x = 0;
for (let i = 0; i < 100000000; i++) {
x++;
}
return count * 2;
};
const doubleCount = useMemo(() => heavyCalc(), [count]); // countが更新された時だけ再計算
return (
<div>
<input
type="text"
onChange={(e) => setName(e.target.value)}
value={name}
/>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<p>Double count: {doubleCount}</p>
</div>
);
};
そうすると、以下のようにフォームに文字列を入力した際の処理速度が 122.3ms から 0.2ms と、かなり早くなりました。画面のもっさり感は跡形もなくなったと思います!👏
count
が更新された際には、適切に heavyCalc()
が再計算されていることも確認できると思います。
このように、useMemo
は処理の再計算に条件をつけることができます。
useCallbackとは何か / 使い所
useCallback
を理解する際には、React内のDOMツリーにおけるルールについて理解しておくとスムーズに理解が進みます。
Reactでは、Stateの値の更新などによって再レンダリングが実行された場合、その子コンポーネントにあたるものも、再レンダリングが実行されます。
これが時に無駄な挙動となりうるので、コンポーネント自体をメモ化することでこれを回避することができます。
const Child = () => {
return <p>子コンポーネント</p>;
};
export const Parent = () => {
const [name, setName] = useState<string>("");
return (
<div>
<input
type="text"
onChange={(e) => setName(e.target.value)}
value={name}
/>
<Child />
</div>
);
};
以下のように、テキストフォームに入力によってState name
が更新された際、Child
コンポーネントも再レンダリングが行われていることが分かります。
ここで使用するのが、「コンポーネントのメモ化」です。
Reactで提供されているmemo
を使用することでコンポーネントをメモ化することができます。
Child
コンポーネントをmemo
で囲ってあげるだけで完了です。
const Child = React.memo(() => {
return <p>子コンポーネント</p>;
});
export const Parent = () => {
const [name, setName] = useState<string>("");
return (
<div>
<input
type="text"
onChange={(e) => setName(e.target.value)}
value={name}
/>
<Child />
</div>
);
};
そうすると、テキストフォームに入力によってState name
が更新されたとしても、Child
コンポーネントも再レンダリングは行われず、Parent
のみ適切に再レンダリングが実行されていることが分かります。
今回の場合、Child
がとてもシンプルなコンポーネントだった為、メモ化を行わなくても目に見えてパフォーマンスが落ちるということはありませんが、たとえばChild
コンポーネントに変更してあげると、メモ化の有無によってパフォーマンスに大きく違いが現れます。
const Child = () => {
let x = 0;
for (let i = 0; i < 100000000; i++) {
x++;
}
return <p>子コンポーネント</p>;
};
では本題の、useCallback
はいつ使うのか。という件です。
もちろんですが、コンポーネントのメモ化もuseMemo
同様に何らかの条件に基づいて再レンダリングは行われます。その条件というのが、「コンポーネント内の状態に変化があった場合」または「propsに変更があった場合」です。
この「propsに変更があった場合」がポイントです。
Reactでは、以下のように子コンポーネントのprops(onChange
)として親コンポーネント側で定義された関数(handleSearch
)を渡すことがあります。
interface ChildProps {
onChange: (text: string) => void;
}
const Child = React.memo(({ onChange }: ChildProps) => {
let x = 0;
for (let i = 0; i < 100000000; i++) {
x++;
}
return <input onChange={(e) => onChange(e.target.value)} />;
});
export const Parent = () => {
const [name, setName] = useState<string>("");
const [searchWord, setSearchWord] = useState<string>("");
const handleSearch = (text: string) => {
setSearchWord(text);
};
return (
<div>
<input
type="text"
onChange={(e) => setName(e.target.value)}
value={name}
/>
<p>Search word: {searchWord}</p>
<Child onChange={handleSearch} />
</div>
);
};
Child
コンポーネントはメモ化されているので、Parent
が再レンダリングされたとしてもChild
は再レンダリングされないと思いきや、
テキストフォームに入力によってState name
が更新された際に、レンダリングが重いChild
も再レンダリングされてしまい、画面がもっさりしてしまいます...
これは、Parent
が更新された際にhandleSearch
が再定義されていることが原因です。
たとえhandleSearch
の内部処理が全く同じだとしても、React上では再定義が行われた時点で、再定義前の関数とは「異なる」という評価がされます。
これによりChild
に渡しているprops onChange
に更新があったと見なされ、Child
が再レンダリングされてしまうということなのです。
こういった、不要な関数の再定義によるパフォーマンス低下の対策として用意されているものが、useCallback
なのです!
使い方は簡単です。第一引数に「再定義されてほしくない関数」を指定して、第二引数に「コイツが更新されたら再定義していいよ。なヤツ」を指定します。
大抵の場合、再定義はされてほしくないので第二引数には空配列を指定することがほとんどです。
interface ChildProps {
onChange: (text: string) => void;
}
const Child = React.memo(({ onChange }: ChildProps) => {
let x = 0;
for (let i = 0; i < 100000000; i++) {
x++;
}
return <input onChange={(e) => onChange(e.target.value)} />;
});
export const Parent = () => {
const [name, setName] = useState<string>("");
const [searchWord, setSearchWord] = useState<string>("");
const handleSearch = useCallback((text: string) => {
setSearchWord(text);
}, []);
return (
<div>
<input
type="text"
onChange={(e) => setName(e.target.value)}
value={name}
/>
<p>Search word: {searchWord}</p>
<Child onChange={handleSearch} />
</div>
);
};
そうすると、テキストフォームに入力によってState name
が更新されParent
が再レンダリングされたとしても、Child
は再レンダリングされずに適切にメモ化が維持され、画面のもっさり感も解消されます!👏
このように、useCallback
は関数の再定義に条件をつけることができます。