Closed16

React.js の再レンダリングの仕組みと Vue.js との違い

ishiyamaishiyama

自分は jQuery → Vue.js → React.js の順でフロントエンドのフレームワークを触ってきた。
自分は Vue.js の時は再レンダリングについてそこまで意識していなかった。だが、React.js の場合は再レンダリングの仕組みを意識していないと、重大なパフォーマンス低下を招く。
React.js(hooks)の再レンダリングのチューニングにおいて、必要なことを記していく。

ishiyamaishiyama

ブラウザの描画フローは「parse」「style」「layout」「paint」「composite」の順番に行われるが、画面の表示内容に更新があった場合は必要な工程のみを処理し、再計算が不要な工程は省略される。
composite 以外はメインスレッドで処理されるが、coposite の処理は別スレッドで行われるため、描画に対する負荷が他より軽い。composite で行われるアニメーション処理としては transform、opacity などがある。逆にメインスレッドを使用するレイアウトプロパティー(left,topなど)の変更は描画に対して重い処理となる。
※ブラウザによって差異あり。

ishiyamaishiyama

DOMツリーの上位コンポーネントでステートを管理している場合、 Change イベントでステートが変更される度に(propsで値を渡しているとか関係なしに)上位コンポーネントを含む全ての下位コンポーネントが再描画される。

沢山の下位コンポーネントを抱えている場合、上位コンポーネントのステート更新は描画パフォーマンスに致命的な負荷を掛けることになる。
この再描画の工程を回避するために React.js ではメモ化という方法でコンポーネントをホワイトリスト形式で定義していく必要がある。

ishiyamaishiyama

React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* render using props */
});

React.memo という HOC で子コンポーネントをラップする。そうすることで、親のステータスが変更されても、そのステータスが子の props に関係がない場合、メモ化した内容が使いまわされ、子の再描画を抑制することができる。

ishiyamaishiyama

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

関数をメモ化するための hooks。コンポーネント内で定義されている関数は、コンポーネントが再描画される度に新しく定義される。つまり、子をメモ化していたとしても props でコールバック関数を渡している場合、props の内容が変わったと認識され、子の再描画を抑制できない。
この場合、関数の再定義を抑制する必要があり、それに使用されるのが useCallback である。

ishiyamaishiyama

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

計算結果をメモ化するための hooks。React.memo と同様にコンポーネントのメモ化に使用できる。コンポーネントの処理が多くなってくると、可読性の観点からメモ化はコンポーネントの外側で行いたいことがある。だが useMemo は hooks なので関数の外では使用できない。React.memo との使い分けはそこ辺りになりそう。
また、計算コストが高い変数をメモ化することもできる。

ishiyamaishiyama

ちなみに関数が再定義されてしまうのはコンポーネント内の話なので、コンポーネントの外で定義された関数で再描画はおこらない。

const alert = () => alert()
const MyComponent = () => {
  return <buttun onClick={alert}>Click me</button>
}

コンポーネントのステートに依存する場合は useCallback。そうでない関数はコンポーネントの外で定義するのも良いだろう。(この場合、コンポーネントに依存していない関数というヒントが得られるので、可読性の向上も見込める)

ishiyamaishiyama

メモ化はそれなりにコストが掛かるのと、可読性にも影響はでてくる。よって描画のコストが高いコンポーネントに使うのが良いとされている。
メモ化するコンポーネントの切り分けには以下のブログが参考になった。
https://cam-inc.co.jp/p/techblog/530551646413914939

ishiyamaishiyama

Vue では、コンポーネントの依存関係が描画中に自動的に追跡されるため、システムは状態が変化したときに実際にどのコンポーネントを再描画する必要があるか正確に認識します。各コンポーネントは、自動的に shouldComponentUpdate が実装されていると見なすことができ、ネストされたコンポーネントに注意する必要はありません。

ishiyamaishiyama

つまり、Vue.jsでは依存関係の管理をフレームワークが行なっており、依存関係が更新されたときのみ再描画が行われている。
ゆえに、Vue.js では再描画を意識しなくてもそこまでパフォーマンスに影響はでなかった。
(逆に、「更新されない!」などの不具合は何度か見かけた。)

ishiyamaishiyama

useRef

const inputRef = useRef<HTMLInputElement>(null);
return <input type="text" ref={inputRef} />;

useRef にはDOM操作の他に状態保持の機能がある。input 要素で ref を指定して値の制御する場合、入力を行っても再レンダリングは行われない。

良記事
https://reffect.co.jp/react/react-hook-form

このスクラップは2022/10/25にクローズされました