概要
再レンダリングのパフォーマンスを最適化する一般的な方法は、不要な作業をスキップすることです。たとえば、キャッシュされた計算を再利用したり、前回のレンダリング以降にデータが変更されていない場合は再レンダリングをスキップするようにReactに指示することができます。
計算や不要な再描画をスキップするには、これらのHooksのいずれかを使用します。
useMemo
高価な計算結果をキャッシュすることができます。
import { useMemo } from 'react';
function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}
useMemoには2つ引数を取る。
1つ目は、 ()=>
引数を取らなず、計算したい内容を返すもの。
2つ目は、計算で使用されるコンポーネント内のすべての値を含む依存配列。
useMemoはレンダリングごとに、依存配列を比較して変更がない場合には以前に計算した値を返します。そうでない場合は再計算して新しい値を返します。
つまり、useMemoは依存配列が変更されるまで再レンダリング間で計算結果をキャッシュしてくれているわけです。
useMemoが有効な場合
デフォルトでは、Reactは再レンダリングのたびにコンポーネントの本体全体を再実行します。
ほとんどの計算は高速に行われるため、問題ありませんが、大きい配列やフィルタリング等の高価な計算を行う場合にはデータが変化していなければ再計算を省略することができます。
### どうやって高価な計算化どうか見分けるか
console.time
とconsole.timeEnd
を利用することで、その間の計算がどのくらいの時間を要するかが確認できます。
console.time('filter array');
const visibleTodos = filterTodos(todos, tab);
console.timeEnd('filter array');
もしこの時間がかなり要する場合(1ms以上など)メモ化する意味があるかもしれないです。
useMemoは最初のレンダリングを速くするわけではなく、更新時に不要な処理をスキップするようにするのに注意が必要です。
useMemoを使用した例
filterTodos
をuseMemoでメモ化しています。
ここではtabを変更すると再計算がされ遅いですが、themeを変更しても再レンダリングの際に遅くなっていないことがわかります。
コンポーネントの再レンダリングスキップ
useMemoは子コンポーネントの再レンダリングのパフォーマンスを最適化できることもあります。
以下の例で、theme
を切り替えると一瞬アプリがフリーズしてしまいます。こういう場合にuseMemoによる高速化を試してみる価値があるかもしれません。
この例の場合では、themeを切り替えた際には、Listコンポーネントのpropsの内容は変化しません。よって、再レンダリングの際にこのコンポーネント自体は差分はないので、memo関数でラップすることによって再レンダリングをスキップするようにします。
import { memo } from 'react';
const List = memo(function List({ items }) {
// ...
});
このListコンポネントの親コンポーネントを見てみましょう。
Listコンポーネントに渡しているvisibleTodos
はuseMemoでラップしてあげる必要があります。
ラップをしなければ、再レンダリングの際にvisibleTodos
は再レンダリング前と違うオブジェクトを作成してしまうため、Listコンポーネントをmemo化していても再レンダリングしてしまうためです。なので、memo化したコンポーネントに渡すpropsの値もしっかりと確認し、内容が同じでも別オブジェクトとして扱われないようにしましょう。
export default function TodoList({ todos, tab, theme }) {
// Tell React to cache your calculation between re-renders...
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // ...so as long as these dependencies don't change...
);
return (
<div className={theme}>
{/* ...List will receive the same props and can skip re-rendering */}
<List items={visibleTodos} />
</div>
);
}
他のHooksをmemo化する
コンポーネントのpropsに依存しているオブジェクトを生成している例を用います。ここではsearchOptions
です。またこのsearchOptions
は下のvisibleItems
の依存配列の一部でもあります。
function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text };
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
// ...
以下のようにtext propsが変更されなければスキップとするようなmemo化を行えばこの問題は解決できます。
function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); // ✅ Only changes when text changes
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes
// ...
以下のように、visibleItemsにラップしてしまうと更に可読性が高くコード量も減らすことができます。
function Dropdown({ allItems, text }) {
const visibleItems = useMemo(() => {
const searchOptions = { matchMode: 'whole-word', text };
return searchItems(allItems, searchOptions);
}, [allItems, text]); // ✅ Only changes when allItems or text changes
// ...
関数のmemo化
以下の例では、Formコンポーネントはmemo化されたコンポーネントであるとします。
Formコンポーネントの親であるProductPageが再レンダリングされる際に、handleSubmit
関数が再作成されてしまい、memo化したFormコンポーネントはスキップされることなく再レンダリングされてしまいます。
export default function ProductPage({ productId, referrer }) {
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}
return <Form onSubmit={handleSubmit} />;
}
handleSubmitに使用されるpropsであるproductIdとreferrerを依存配列としたuseMemoでラップしてあげましょう。
export default function Page({ productId, referrer }) {
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post('/product/' + product.id + '/buy', {
referrer,
orderDetails
});
};
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
しかし、() => {}
のような無名関数でラップしなければならないという欠点があります。
この問題に対応するためにReactにはuseCallback
という関数をmemo化するためのHooksが用意されています。
以下のように記述することができ、より可読性が高くなります。
export default function Page({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + product.id + '/buy', {
referrer,
orderDetails
});
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
参考