みなさんがReactを触る際に、パフォーマンスチューニング対応でよくやるのが「レンダリング」の状況を確認することだと思います。
レンダリングのことを把握することでReactのパフォーマンスチューニングができるようになるということですね。
今回はレンダリングの発生からレンダリングを最適化するための方法を整理していきたいと思います。
整理した上で、しっかりと実務に反映できるように...
Reactがレンダリングされる条件
そもそも、Reactがどういった条件でレンダリングするかを把握しなければなりません。
大きく分けると3条件となります。
- stateが更新された時
- propsが更新された時
- 親コンポーネントが再レンダリングされた時
※サンプルについてはNext.jsとなります
stateの更新
import type { NextPage } from 'next';
import React, { useState } from 'react';
const Home: NextPage = () => {
console.log('レンダリング');
const [text, setText] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText((prevState) => e.target.value);
};
return (
<div>
<h2>レンダリング確認</h2>
<input type='text' name='' id='' onChange={handleChange} />
</div>
);
};
export default Home;
inputの値を変更するとstateが更新されるため、レンダリングが都度走っていることが確認できるかと思います。
propsが更新
Child
import react from 'React';
const SubInput: React.FC<{ text: string }> = ({ text }) => {
console.log('コンポーネントの更新');
return (
<>
<p>子コンポーネントの更新</p>
<p>{text}</p>
</>
);
};
export default SubInput;
親ファイルに下記を追加
<SubInput text={text} />
stateを子コンポーネントに渡しており、子コンポーネントのpropsが更新されるたびにレンダリングされているのが確認できるかと思います。
親コンポーネントが再レンダリングされた時
親コンポーネントで再レンダリングが発生すると、その配下にある子コンポーネントが全て再レンダリングされてしまうと言うことになります。
先程stateの更新で親コンポーネントがレンダリングされているため、Propsを持たない子コンポーネントまでレンダリングされると言う内容です。
Propsを持たないChildを追加し、レンダリングされることを確認
const Sub: React.FC = () => {
console.log('Propsないけどコンポーネントが更新');
return (
<>
<p>Propsがない子コンポーネントの更新</p>
</>
);
};
export default Sub;
このようにReactではレンダリングされる条件をきちんと把握することがパフォーマンス改善の第一歩となります。
実際に再レンダリングによる最適化については別途ご紹介したいと思います。
refについて詳しく知る
話が逸れますが、Reactにはrefというものがあります。
Reactでフォームライブラリといえば、React Hook Formですが、React Hook Formのパフォーマンスが高い理由として、下記のように記載されています。
非制御コンポーネントによってregister関数をrefで実行しています。
そもそもrefってなんぞや?と言うことですが...実は私もあまり理解できていません。
簡単に説明します。
本来のデータはpropsを用いて親コンポーネントから子コンポーネントにデータを渡すのが基本です。
それ以外の方法で、子コンポーネントのDOMの操作を行う際に、refが用いられることになります。
refはオブジェクトを生成し、子コンポーネントなどのDOMにref属性を入れることで、紐づけることができるようになります。
要するに、refとはDOMの参照を保持するということを意味しています。
例えば、refを扱うためのHooksとしてuseRefがあります。
import type { NextPage } from 'next';
import React, { useEffect, useRef, useState } from 'react';
import Sub from '@/components/Sub';
import SubInput from '@/components/SubInput';
const Home: NextPage = () => {
const el = useRef(null);
const [text, setText] = useState('');
useEffect(() => {
console.log(el.current);
}, [text]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText((prevState) => e.target.value);
};
return (
<div>
<h2>レンダリング確認</h2>
<input type='text' name='' id='' onChange={handleChange} />
<SubInput text={text} />
<Sub />
<p ref={el}>{text}</p>
</div>
);
};
export default Home;
el.currentとすることで現在の要素を取得可能ですので、p要素にアクセスが可能と言うことになります。
また、こちらについては子コンポーネントに渡すことで、子コンポーネントの操作ができるようになります。
※関数コンポーネントの時は注意で、コンソールにエラーを吐きます。React.forwardRef()を使ってrefを受け取れるようにしてあげる必要があります。
refを使えば、下記のようにボタンクリックしたらinputにfocusを当てる
ということも可能です。
import type { NextPage } from 'next';
import React, { useEffect, useRef, useState } from 'react';
import Sub from '@/components/Sub';
import SubInput from '@/components/SubInput';
const Home: NextPage = () => {
const el = useRef(null);
const [text, setText] = useState('');
useEffect(() => {
console.log(el.current);
}, [text]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText((prevState) => e.target.value);
};
const clickButton = () => {
el.current && el.current.focus();
};
return (
<div>
<h2>レンダリング確認</h2>
<input type='text' name='' id='' onChange={handleChange} ref={el} />
<SubInput text={text} />
<Sub />
<button onClick={clickButton}>クリックしてinputにフォーカスを当てる</button>
</div>
);
};
export default Home;
refのいいところは、更新によるレンダリングは発生しないことです。
なので、React Hook Formではレンダリング数が抑えられていると言うわけです。
レンダリングとパフォーマンスの関係
レンダリング回数の把握は、パフォーマンスチューニングにおいてとても重要となります。
例えば、レンダリングが発生する条件としてstateが更新された時と説明しました。
import type { NextPage } from 'next';
import React, { useState } from 'react';
const Home: NextPage = () => {
const [text, setText] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText((prevState) => e.target.value);
};
return (
<div>
<input type='text' name='' id='' onChange={handleChange} />
</div>
);
};
export default Home;
上記のような構文では、inputの値に何かしらの入力をする度にstateが更新されています。
入力ごとにstateが更新=レンダリングが走るのは無駄ですね...
今1コンポーネントだけの描画になっていますが、これが子コンポーネントとして複数コンポーネントを持っているとした場合、子コンポーネントは親コンポーネントがレンダリングされたことにひっぱられ、不必要に子コンポーネントがレンダリングされることになるので、さらに無駄が発生しています。
コンポーネントのメモ化(memo)
上記のような子コンポーネントの不必要なレンダリングを防ぐための技術として「メモ化」があります。
まずは現状はこのような感じで、親のstateが更新→親コンポーネントがレンダリング→子もレンダリングという状況です。
親
import type { NextPage } from 'next';
import React, { useState } from 'react';
import Sub from '@/components/Sub';
const Home: NextPage = () => {
const [text, setText] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText((prevState) => e.target.value);
};
console.log('親レンダリング');
return (
<div>
<h2>レンダリング確認</h2>
<input type='text' name='' id='' onChange={handleChange} className='border' />
<Sub />
</div>
);
};
export default Home;
子
const Sub: React.FC = () => {
console.log('子レンダリング');
return (
<>
<p>Propsがない子コンポーネントの更新</p>
</>
);
};
export default Sub;
そこで子コンポーネントをmemoでラップしてあげます。
import React, { memo } from 'react';
const Sub: React.FC = memo(() => {
console.log('子レンダリング');
return (
<>
<p>Propsがない子コンポーネントの更新</p>
</>
);
});
export default Sub;
すると、メモ化した子コンポーネントは初期レンダリングのみでその後レンダリングが走っていないことがわかります。
メモ化することで、子コンポーネントはpropsに更新が発生した時だけレンダリングが発生するようになります。
関数のメモ化(useCallback)
例えば、子コンポーネントに対し、関数をpropsとして渡す時を考えてみましょう。
import type { NextPage } from 'next';
import React, { useState } from 'react';
import Sub from '@/components/Sub';
const Home: NextPage = () => {
const [text, setText] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setText((prevState) => e.target.value);
};
const handleClick = () => {
// TODO
};
console.log('親レンダリング');
return (
<div>
<h2>レンダリング確認</h2>
<input type='text' name='' id='' onChange={handleChange} className='border' />
<Sub onClick={handleClick} />
</div>
);
};
export default Home;
子コンポーネントに対して、handleClickの関数をpropsとして渡しました。
import React, { memo } from 'react';
type Props = {
onClick: () => void;
};
const Sub: React.FC<Props> = memo(({ onClick }) => {
console.log('子レンダリング');
return (
<>
<p>Propsがない子コンポーネントの更新</p>
<button onClick={onClick}>クリック</button>
</>
);
});
export default Sub;
この状態でinputの中に値を入れ、stateを更新していきましょう。
stateが更新されるため、親コンポーネントはレンダリングが発生します。
しかし、この場合先ほどの例で言うなら、子コンポーネントをメモ化しておけば子コンポーネントではレンダリングが発生しないはずですが、今回は子コンポーネントがレンダリングしています。
なぜこのようになってしまったと言うと、子コンポーネントにpropsとして渡した関数が原因となります。
関数であるhandleClick
関数ですが、初期レンダリングで定義されたものが使いまわされているように見えるのですが、実はレンダリングする毎にhandleClick
関数を生成しています。
関数が毎回新しく生成されていることから、子コンポーネントでは、propsが更新されたと判断したため、レンダリングが発生していると言うことになります。
そこで関数をメモ化させるuseCallback
というものを利用します。
const handleClick = useCallback(() => {
// TODO
}, []);
第一引数にコールバック関数を指定し、第二引数に依存配列で要素を指定します。
配列に指定した要素がある場合、その要素に変更がかかった場合、この関数が再生成されるという内容です。
上記のように空配列にしておくと、一度しか生成しなくなります。
こうすることで子コンポーネントのレンダリングを抑えることができるようになりました。
変数のメモ化(useMemo)
最後に変数をメモ化させることも可能です。
const sum = (num1, num2) => {
return num1 + num2;
};
こちらをメモ化させる場合
const sum = useMemo((num1, num2) => {
return num1 + num2;
}, []);
上記のように空配列を第二引数に渡してあげることでレンダリング毎に計算をすることはなく、初回のみ計算を実行しその後のレンダリングでは値を使い回すことになります。
memoやuseCallback利用の判断
子コンポーネントをメモ化するべきタイミングはどうすればいいかということですが、再レンダリングによる描画が不要なもので判断すれば良いかと思います。
例えば、動的に値が変わることのない静的なコンポーネント、propsが変数だけといったコンポーネントに適用させることが最適化の一つと言えます。
まとめ
特に意識せずコードを書いてしまうと、レンダリングの無駄が発生してしまっていることも少なくありません。
パフォーマンスの最適化のため、上記のようなメモ化の技術を用いながら最適化できるようにしていきたいと思います。