Reactで値や関数をメモ化してパフォーマンスを改善してみる(React.memo, useMemo, useCallback)
最終更新日: Update!!
Reactでは親コンポーネントのstateやpropsの値などが更新されると、それに紐づく子コンポーネントがレンダリングされてしまいます。もちろん、子コンポーネント側で受け取る場合には再レンダリングされることで、コンポーネントの情報も新しく更新されるのですが、関係のないコンポーネントまでレンダリングされるため、大量のコンポーネントを読み込んでいる場合では、パフォーマンスに影響が出てしまいます。そうならないようなコンポーネント設計も重要ですが、Reactでは「React.memo」「useMemo」「useCallback」という、コンポーネントや処理などを「メモ化」する機能が用意されています。Reactでの開発はこれらの活用が欠かせません、今回はサンプルと合わせて使い分けや違いについて改めてチェックしてみたいと思います。
Reactのメモ化というのは、処理や値を再利用できるような形で保持するような機能で、個人的にはキャッシュのようなイメージと捉えています。メモ化をすることで、重い計算処理を無駄に何度も繰り返すことや、不要なコンポーネントのレンダリングを防ぐなどの役割を果たします。つまり、重たくなりがちな場面でのパフォーマンスが期待できるようになります。
似ているようですが、それぞれ適切に扱うことで処理のパフォーマンスを改善することが期待できますので、普段はVue.jsを使うことが多いのですが、Reactでのアプリケーション開発ではぜひ覚えておきたいですね。
React.memoで親要素のレンダリングに伴う子コンポーネントの不要なレンダリングを防ぐ
似たよう機能と名前でややこしいのですが、まずは「React.memo」を使ってみます。React.memoはコンポーネント自体をメモ化するもので、主に不要なレンダリングを避けたい子コンポーネントに対して適用していきます。 まずは適用前のサンプルを見ていきます。今回は親コンポーネントに、チェックボックスとレンジのinput要素を用意し、値を子コンポーネント側で表示するというようなものになります。親コンポーネント側では「useState」でインプット要素の値が動的に変化できるようにしておきます。そして、レンジの値を子コンポーネント側にPropsとして受け渡します。 【ParentComponent.tsx】import React from 'react'; import { ChildComponent } from './ChildComponent'; export const ParentComponent: React.VFC = (): JSX.Element => { const [currentRange, setRange] = useState(0); const [isChecked, setCheck] = useState(false); const toggleCheck = (): void => { setCheck(!isChecked); }; const updateRange = (event: React.ChangeEvent<HTMLInputElement>): void => { setRange(Number(event.target.value)); }; console.log('ParentComponent rendered!'); return ( <> <label style={{display: 'block'}}> <input type="checkbox" onChange={() => {toggleCheck()}} /> </label> <label style={{display: 'block'}}> <input type="range" min="0" max="100" step="10" defaultValue={0} onChange={(event) => updateRange(event)} /> </label> <ChildComponent data={currentRange} /> </> ); }子コンポーネントでは、親コンポーネントからPropsで受け取ったレンジの値を出力するというような形です。ここではレンダリングされたことがわかるようにコンソールにログが表示されるようにしておきます。親コンポーネント側でレンジの値が変更されるたびに、子コンポーネント側で値が更新されるような感じですね。 【ChildComponent.tsx】
import React from 'react'; export type childProps = { data: number }; export const ChildComponent: React.VFC<childProps> = (props): JSX.Element => { console.log('ChildComponent rendered!'); return ( <> <div>値:{props.data}</div> </> ); };レンジの値を切り替えると子コンポーネントがレンダリングされるのは必要になりますが、上記の場合、子コンポーネントとは関係のないチェックボックスの値が更新された時でも子コンポーネントがレンダリングされてしまいます。親コンポーネント側で保持するstateの値が更新されたためですね。コンソールでは下記にように子コンポーネントがレンダリングされているのが確認できますね。
// console(チェックボックスを切り替えたとき) ParentComponent rendered! ChildComponent rendered!コンポーネントの構造が複雑だったり数が多い場合には、このようなレンダリングが原因で動作や表示が重たくなることもあります。そんな場合には、子コンポーネントに「React.memo」を使って、コンポーネント自体をメモ化することで不要なレンダリングを防ぐことができます。関数コンポーネントの場合ですと、引数にそのままコンポーネントの関数が入る形になります。 【ChildComponent.tsx】※一部抜粋
export const ChildComponent: React.VFC<childProps> = React.memo((props): JSX.Element => { console.log('ChildComponent rendered!'); return ( <> <div>値:{props.data}</div> </> ); });こうすることで、コンポーネントでPropsとして受け取らない値が更新された場合には、子コンポーネントがレンダリングされないようになります。コンソールでも確認することができました。
// console(チェックボックスを切り替えたとき) ParentComponent rendered!
React.memoとuseCallbackでPropsで受渡す関数をメモ化して子コンポーネントの不要なレンダリングを防ぐ
子コンポーネントには値だけでなく関数もPropsとして受け渡すこともあります。下記の例では、レンジのインプット要素とレンジの値を取得して更新する処理を子コンポーネント側に持たせるパターンです。そのため、親コンポーネント側で定義している関数をPropsとして子コンポーネントに受け渡しています。また、今回のようにuseCallbackを使う場合にはモジュールからインポートしておきます。 【ParentComponent.tsx】import React from 'react'; import { useCallback, useState } from 'react'; import { ChildComponent } from './ChildComponent'; export const ParentComponent: React.VFC = (): JSX.Element => { const [currentRange, setRange] = useState(0); const [isChecked, setCheck] = useState(false); const toggleCheck = (): void => { setCheck(!isChecked); }; const updateRange = (event: React.ChangeEvent<HTMLInputElement>): void => { setRange(Number(event.target.value)); }; console.log('ParentComponent rendered!'); return ( <> <label style={{display: 'block'}}> <input type="checkbox" onChange={() => {toggleCheck()}} /> </label> <ChildComponent data={currentRange} methods={updateRange} /> </> ); }子コンポーネント側では、受け取った関数をコンポーネント内で使われるインプット要素のイベントで実行されるように使います。これで、子コンポーネントでのイベントで値を受け取り、その値が親コンポーネント側のstateで更新されるようになります。 【ChildComponent.tsx】
import React from 'react'; export type childProps = { data: number, methods: (event: React.ChangeEvent<HTMLInputElement>) => void }; export const ChildComponent: React.VFC<childProps> = React.memo((props): JSX.Element => { console.log('ChildComponent rendered!'); return ( <> <label style={{display: 'block'}}> <input type="range" min="0" max="100" step="10" defaultValue={0} onChange={(event) => props.methods(event)} /> </label> <div>値:{props.data}</div> </> ); });ただし、先ほどと同様に親コンポーネント側で子コンポーネントに関与しないstateの値が更新された場合に、React.memoでコンポーネント自体をメモ化していても、関数自体は親コンポーネントで定義しているため、子コンポーネントがレンダリングされてしまいます。
// console(チェックボックスを切り替えたとき) ParentComponent rendered! ChildComponent rendered!このように、子コンポーネントに関数を受け渡す際に活用できるのが「useCallback」です。useCallbackは関数をメモ化することで、対象とする値が更新された時にしか実行しないように処理を制御することができ、不要な場面で処理が実行されないようにします。useCallbackでは第一引数に対象とする処理を、第二引数に処理を実行する対象となる配列値を指定します。第二引数で指定した値が更新された時にしか、第一引数の処理が実行されないようになります。
useCallback( メモ化する対象の処理, 更新対象の値 )実際には下記のようになります。親コンポーネント側で定義されているので、useCallbackの引数にメモ化する処理を指定し、第二引数で更新の対象となるstateの値を指定します。これで第二引数で指定した値が更新されない限り、第一引数の処理が実行されなくなります。 【ParentComponent.tsx】※一部抜粋
export const ParentComponent: React.VFC = (): JSX.Element => { const [currentRange, setRange] = useState(0); const [isChecked, setCheck] = useState(false); const toggleCheck = (): void => { setCheck(!isChecked); }; const updateRange = useCallback((event: React.ChangeEvent<HTMLInputElement>): void => { setRange(Number(event.target.value)); }, [currentRange]); console.log('ParentComponent rendered!'); return ( <> <label style={{display: 'block'}}> <input type="checkbox" onChange={() => {toggleCheck()}} /> </label> <ChildComponent data={currentRange} methods={updateRange} /> </> ); }実際にPropsで受け渡していない値が変更された時には子コンポーネントがレンダリングされていないことが確認できます。ただし、注意点としてuseCallbackを使う際には、受け渡す側の子コンポーネントで、React.memoなどでコンポーネント自体もメモ化しておく必要があります。
// console(チェックボックスを切り替えたとき) ParentComponent rendered!
React.memoとuseMemoで値を返す関数の再計算と紐づく子コンポーネントのレンダリングを防ぐ
コンポーネント内では、受け取った値や受け渡す値を計算して、HTML上に出力することもあります。軽い処理では問題ありませんが、重い処理がたくさん実行されるような場合には、不要なレンダリングで処理が再計算されることが原因でパフォーマンスにも影響が出てしまいます。受け渡す値がそのような処理で算出された値の場合には、できるだけ不要なレンダリングを避けたいですね。下記のサンプルでは、親コンポーネント側で値を加算、そして乗算するという異なる処理が行われるパターンになります。そして、それぞれの処理で算出された値は異なる子コンポーネントに受け渡すというような構成です。 【ParentComponent.tsx】import React from 'react'; import { useMemo, useState } from 'react'; import { ResultPlus } from './ResultPlus'; import { ResultMultiplie } from './ResultMultiplie'; export const ParentComponent: React.VFC = (): JSX.Element => { const [currentRangePlus, setRangePlus] = useState(0); const [currentRangeMultiplie, setRangeMultiplie] = useState(0); const updateRangePlus = (event: React.ChangeEvent<HTMLInputElement>): void => { setRangePlus(Number(event.target.value)); }; const updateRangeMultiplie = (event: React.ChangeEvent<HTMLInputElement>): void => { setRangeMultiplie(Number(event.target.value)); }; const calculatePlus = (): number => { console.log('加算') return currentRangePlus + 10; }; const calculateMultiplie = (): number => { console.log('乗算') return currentRangeMultiplie * 10; }; return ( <> <label style={{display: 'block'}}> <input type="range" min="0" max="100" step="10" defaultValue={0} onChange={(event) => updateRangePlus(event)} /> </label> <label style={{display: 'block'}}> <input type="range" min="0" max="100" step="10" defaultValue={0} onChange={(event) => updateRangeMultiplie(event)} /> </label> <ResultPlus data={calculatePlus} /> <ResultMultiplie data={calculateMultiplie} /> </> ); }子コンポーネントでは、受け取った値をHTML上に出力しています。親コンポーネント側で定義されている計算処理と子コンポーネント側では、処理の実行やコンポーネントのレンダリングなどがわかるようコンソールに出力されるようにしています。
// ResultPlus.tsx import React from 'react'; export type childProps = { data: () => number }; export const ResultPlus: React.VFC<childProps> = React.memo((props): JSX.Element => { console.log('ResultPlus rendered!'); return ( <> <div>+10:{props.data()}</div> </> ) }); // ResultMultiplie.tsx import React from 'react'; export type childProps = { data: () => number }; export const ResultMultiplie: React.VFC<childProps> = React.memo((props): JSX.Element => { console.log('ResultMultiplie rendered!'); return ( <> <div>*10:{props.data()}</div> </> ) });本来ですと、それぞれのコンポーネントに紐づく値が更新されたものだけ対応するコンポーネントのレンダリングや処理が実行されてほしいところですが、両方ともの処理が実行されてしまっているのがコンソールで確認できます。
// console(加算処理用のレンジを操作した時) ResultPlus rendered! 加算 ResultMultiplie rendered! 乗算そこで「useMemo」を使って各計算処理で算出された値をメモ化しておきます。先ほどのuseCallbackと同じ要領で、第一引数には処理を、第二引数には更新対象になる配列値を指定しておきます。
useMemo( メモ化する対象の処理, 更新対象の値 )第二引数で指定した値が更新されない限りは再計算されないという形になります。先ほどの親コンポーネントに対して下記のように対象となる処理にuseMemoを適用していきます。第二引数にはそれぞれ紐づくstateの値を指定します。 【ParentComponent.tsx】※一部抜粋
export const ParentComponent: React.VFC = (): JSX.Element => { const [currentRangePlus, setRangePlus] = useState(0); const [currentRangeMultiplie, setRangeMultiplie] = useState(0); const updateRangePlus = (event: React.ChangeEvent<HTMLInputElement>): void => { setRangePlus(Number(event.target.value)); }; const updateRangeMultiplie = (event: React.ChangeEvent<HTMLInputElement>): void => { setRangeMultiplie(Number(event.target.value)); }; const calculatePlus = useMemo(() => (): number => { console.log('加算') return currentRangePlus + 10; }, [currentRangePlus]); const calculateMultiplie = useMemo(() => (): number => { console.log('乗算') return currentRangeMultiplie * 10; }, [currentRangeMultiplie]); return ( <> <label style={{display: 'block'}}> <input type="range" min="0" max="100" step="10" defaultValue={0} onChange={(event) => updateRangePlus(event)} /> </label> <label style={{display: 'block'}}> <input type="range" min="0" max="100" step="10" defaultValue={0} onChange={(event) => updateRangeMultiplie(event)} /> </label> <ResultPlus data={calculatePlus} /> <ResultMultiplie data={calculateMultiplie} /> </> ); }これでそれぞれの処理で算出された値がメモ化され、更新されない場合には再利用される形になります。処理が再計算されたり、値の受け渡し先の子コンポーネントもレンダリングされていないことがコンソールで確認できます。
// console(加算処理用のレンジを操作した時) ResultPlus rendered! 加算これらのパフォーマンス改善に活用できる各種メソッドをまとめてみると下記のようになります。
React.memo | コンポーネントをメモ化して不要なレンダリングを防ぐ、コンポーネント自体に適用する |
---|---|
useCallback | 関数をメモ化して処理自体を再利用できるようにする、Propsで受け渡す関数などに適用する |
useMemo | 返り値をメモ化して、処理の再計算を防いだり値を再利用できるようにする、Propsで受け渡す値などに適用する |
似ているようですが、それぞれ適切に扱うことで処理のパフォーマンスを改善することが期待できますので、普段はVue.jsを使うことが多いのですが、Reactでのアプリケーション開発ではぜひ覚えておきたいですね。
sponserd
keyword search
recent posts
- ViteでMarkuplintとPrettierを使える環境を構築する
ViteでMarkuplintとPrettierを使える環境を構築する
- ViteでStylelintとESlintを使える環境を構築する
ViteでStylelintとESlintを使える環境を構築する
- マウスオーバーしたセルを含む行列がハイライトするテーブルを作成する:has()擬似クラスの活用例
マウスオーバーしたセルを含む行列がハイライトするテーブルを作成する:has()擬似クラスの活用例
- ViteでVue.jsとVuex(Pinia)とVue Routerを使ってみる
ViteでVue.jsとVuex(Pinia)とVue Routerを使ってみる
- ViteでHandlebarsを使った複数ページの作成に使える外部JSONファイルのデータを読み込む
ViteでHandlebarsを使った複数ページの作成に使える外部JSONファイルのデータを読み込む
- ViteでTailwindCSSとテンプレートエンジンのHandlebarsを使ったページコーディング
ViteでTailwindCSSとテンプレートエンジンのHandlebarsを使ったページコーディング
- ViteでPostCSS周りの設定やSassを使う
ViteでPostCSS周りの設定やSassを使う
- フロントエンドの開発環境にVite + TypeScriptを導入する
フロントエンドの開発環境にVite + TypeScriptを導入する
categories