React のレンダリングアルゴリズムはざっくりいうと「コンポーネントの持つデータに差分があり次第、再レンダリングする」というものです。この差分ですが JavaScript における Object の扱いとあいまって無駄に再レンダリングしたり全く再レンダリングしなかったりとままならないことがしばしばあります。このつながりで期待通りに動いているプログラムであっても同じプロパティと値を持つ異なるオブジェクトを扱っているため無駄に計算が増え、ページが重くなっているということがあります。こういった時にはどこで何が原因で無意味な再レンダリングが起きているかを探し、それを修正するべきです。 Why Did You Render はこの問題が起きている部分を検知し、コンソールに表示してくれるデバッグツールです。
これは React に機能を追加するイメージで使います。例によってnpm install @welldone-software/why-did-you-render --save-devでインストールし、次の様に呼び出します。
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import ErrorBoundary from '@/common/ErrorBoundary';
import { AppRouter } from '@/account/router/Router';
import whyDidYouRender from '@welldone-software/why-did-you-render';
// 全ての処理の前に React を whyDidYouRender に渡す
// 処理が重くなるので開発環境でのみ適用するために開発課尿限定条件分岐の中で渡す
if (process.env.NODE_ENV === 'development') {
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
/**
* ルートコンポーネント
*/
const App = () => {
return (
<ErrorBoundary>
<React.StrictMode>
<BrowserRouter basename={'account'}>
<AppRouter />
</BrowserRouter>
</React.StrictMode>
</ErrorBoundary>
);
};
ReactDOM.render(<App />, document.getElementById('app'));
こんな感じで whyDidYouRender を入れるとコンソールに次の様な検査結果が出力されるようになります。
画像の中では”different objects that are equal by value. (more info at http://bit.ly/wdyr02)”と同じ値を持つ異なるオブジェクトによって無駄にレンダリングしていると教えてられています。この様に無駄なレンダリングを探し、問題となっているレンダリングを制御することでより快適な画面が作れます。
具体的にどうレンダリングを抑制するかですが、主に使うのは React.memo、useMemo、useCallback 、個別の条件分岐でレンダリングをするしないを明示します。最適な条件は個々のケースがありますが、手っ取り早いのは deepEqual で検索してでてくる方法で単純な === でなく、オブジェクトの中身で値が前回レンダリング時と同じなのか異なるかを判別する方法です。最適でないことも多いですが無暗にレンダリングするよりよっぽど早くなります。例えば次の様にできます。
// npm install react-fast-compare
// @see https://github.com/FormidableLabs/react-fast-compare
// 上記リポジトリからコードを引用しています
import isEqual from 'react-fast-compare';
// 使い方
console.log(isEqual({ foo: "bar" }, { foo: "bar" })); // true
// React.memo
// ExpensiveComponentが再レンダリングされるのは、propsが異なる値を示す様に変化した時だけになります
const DeepMemoComponent = React.memo(ExpensiveComponent, isEqual);
// React.Component shouldComponentUpdate
// AnotherExpensiveComponent が再レンダリングされるのは、propsが異なる値を示す様に変化した時だけになります
class AnotherExpensiveComponent extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
render() {
// ...
}
}
上記例では react-fast-compare というライブラリを用いています。これは高速な比較を React に適した形で行ってくれるライブラリです。
FormidableLabs/react-fast-compare: fastest deep equal comparison for React
こんな感じで React が用意してくれたオブジェクトの同一性関連のロジックにオブジェクトの中身を比較する関数をあてはめることで無用なレンダリングを抑制できます。
もしこの辺りのレンダリング抑制を用いてなお遅い場合はコンポーネントの細分化を試みやアルゴリズムの見直しを行うべきです。そういった場合はとりあえずループで処理しているところを軽くするのがベターです。ツリー状のデータやコンポーネントにして計算量O(1)を目指したり、処理を削ったりでループそのものを減らすのがコツです。
