ここ数日 React について調べてきたのでようやく「見た目だけは作れます」という情けない状態を脱しつつあるのではないかなと思います。
今回は Web アプリを作るのに欠かせないパフォーマンスの最適化について調べていきます。私はだいたい読み込みに7秒以上かかるとブラウザバックしてます。せっかく作ったアプリが誰にも使われなかったらと思うといたたまれないですね…しっかり勉強しておきましょう!
公式ドキュメントで推奨されている方法
まずは 公式ドキュメント を見ていきましょう。
推奨されているのは以下です。
- 本番用ビルドを使用する
- 長いリストの仮想化
- リコンシリエーション(差分検出処理)を避ける
特定の環境での最適化は今回飛ばしました(理由はただ私が create-react-app
ばかり使っているためです)。
1.本番用ビルドを使用する について
本番用ビルドでは React の警告チェックがなくなり、速度が速くなります。
「遅いな…」と思ったら本番用ビルドを使っているかまずは確かめましょう。
2.長いリストの仮想化 について
仮想化とは SNS などの大量の投稿データをフィードで表示するアプリを制作する際に見える件数だけデータを読み込む実装を行うことです。
こちら の記事が大変わかりやすかったです。
3. リコンシリエーション(差分検出処理)を避ける
React は差分を検知し仮想 DOM を変更 → HTML に反映、という動作を行なっています。もし state が変わっても画面を再レンダリングする必要がなければ、再描画させない、という指定が必要になります。
ただし公式ドキュメントでは「多少の時間がかかっても多くの場合は問題にはなりません」と書かれており、先に1、2を確認することが大切でしょう。
これ以降、この記事では再レンダリングや無駄な計算を減らすために使われるフック・関数について見ていきます。
レンダリングの削減に関わるフック・関数
レンダリングの削減に関わるフック・関数として、
React.memo
useCallback
が挙げられます。
さて、次へ進む前に React の再レンダリングについて知っておきましょう。
React の再レンダリングはいつ起こるのか?
- state が更新されたとき
- props が更新されたとき
- 親コンポーネントが再レンダリングされたとき
- 1について: state を更新する state setter function にはレンダリングを引き起こす働きがあるためです。そもそも必要なレンダリングといえるでしょう
- 2について: props のデータが更新されたことによるものなのでこちらも必要なレンダリングです
- 3について: 不要な場合もあります
親コンポーネントに変更があった場合、子コンポーネントには何の変更もなかったとしても再レンダリングが起こってしまいます。今回紹介したいのは 3 のレンダリングを減らす方法です。
React.memo
の使用
React.memo
は高階コンポーネントであり、コンポーネントを受け取って新しいコンポーネントを返します。
React.memo
で子コンポーネントをラップすることで、親コンポーネントからの props に変化がなかった場合はレンダリングさせないようにできます。
import {useState, memo} = "react"
const Parent = () => {
const [count, setCount] = useState(0);
consol.log("親コンポーネントが呼ばれました");
return(
<div>
<div>{count}</div>
<button onClick={onClick}>Click</button>
<ChildComponent />
</div>
);
}
const ChildComponent = memo(() => {
console.log("子コンポーネントが呼ばれました");
return(
<div>This is ChildComponent</div>
);
})
// 引用: https://zenn.dev/nemofilm/articles/9ac490615b805c
通常は浅い比較ですが、第2引数で props を比較する関数を指定することもできます。
親の state 変化によって子を変化させる必要がない場合に使うことができますね。
useCallback
の使用
メモ化されたコールバックを返します。
※ メモ化: 引数が同じであれば同じ結果を返すように関数の実行結果を保存すること。キャッシュの一種。
第一引数にはコールバック、つまり型としては関数を、第二引数には依存要素の入った配列を渡します。React は Object.is を使って、各依存関係をその前の値と比較しているようです。
使うタイミングとしては、子コンポーネントの props に関数がある時です。
import {useState, memo, useCallback} = "react"
const Parent = () => {
const [count, setCount] = useState(0);
const [childCount, setChildCount] = useState(0);
consol.log("親コンポーネントが呼ばれました");
const onClick = () => {
setCount(count + 1);
}
const onClildClick = useCallback(() => {
setChildCount(childCount + 1);
},[childCount])
return(
<div>
<div>{count}</div>
<button onClick={onClick}>Click</button>
<ChildComponent onClick={onClildClick}/>
</div>
)
}
const ChildComponent = memo((props) => {
console.log("子コンポーネントが呼ばれました")
return(
<div>
<div>This is ChildComponent</div>
<button onClick={props.onChildClick}>Click!<button>
</div>
)
})
// 引用: https://zenn.dev/nemofilm/articles/9ac490615b805c
子コンポーネントに関数を渡すと、毎回「新しい関数」を受け取ったように振る舞ってしまいます。そのため親コンポーネントの再レンダリングに伴って子コンポーネントも再レンダリングされます。
useCallback
を使って関数をメモ化しておくことで「既に受け取っている関数」と正しく認識され、不要なレンダリングを削減できます。
計算を減らす useMemo
の使用
useCallback
はメモ化されたコールバックを返しますが、useMemo
はメモ化された値を返します。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
第一引数には関数を、第二引数には依存する値の配列を渡します。引数に限らず関数の内部で参照されている全ての値を配列に入れましょう。この配列も OBject.is
で比較されています。
useMemo
の役割はレンダリングを減らすことではなく、数値処理を代入する変数があるとき、時間がかかる計算をレンダー毎に行うことを避けることです。
注意点として useMemo
はあくまでもパフォーマンス最適化のためのものであり、useMemo
なしでも動作するコードを書かなくてはいけません。例えばメモ化する必要のない副作用は useEffect
(後述)を使って書きましょう。
なお useCallback(fn, deps)
は useMemo(() => fn, deps)
と同じことになります。
useEffect
の第二引数に注意
最後に useEffect
についても少し触れましょう。
useEffect は 公式ドキュメント では「副作用フック」と記載されています。SideEffect の Effect なのでしょう。
useEffect
を使うことで、関数コンポーネントの中で副作用(React DOM を返す以外の動き)を使うことができます。例えばデータの取得、DOM の手動変更などですね。
React が DOM を更新した後に useEffect
が動作し、引数として渡された関数の処理、つまり Effect が実行されます。
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
// 引用: https://beta.reactjs.org/apis/react/useEffect#examples-dependencies
問題は useEffect
に渡される第2引数の配列、依存関係です。
何を指定した場合でも1回は Effect が実行されます。そしてそれ以降の動作は依存関係の指定によって変わってきます。
依存関係の配列は Object.is
で比較され、変更があれば Effect が再実行されます。
ユーザーの操作によって API にアクセスし直さないといけない、サーバーに接続し直さないといけないといったケースが考えられます。
ただし 必要ない値を渡してしまった場合、不要な動作が発生します。
空の配列を指定した場合、初回マウント時のみ Effect が実行されます。1回だけ実行したい時に使えますね。
そして依存関係を全く指定しない場合、コンポーネントを再レンダリングするたびに毎回 Effect が再実行されます。 こちらも不要な動作になり得ますね。
まとめ
- React のパフォーマンスを最適化するため、
- 本番では本番用ビルドを使用する
- 長いリストの仮想化
- リコンシリエーション(差分検出処理)を避ける
- React.memo: props の変更がなければコンポーネントを再レンダリングしないように設定
- useCallback: 関数をメモ化
- useMemo: 値をメモ化
- useEffect の第2引数を指定しない場合、不要なレンダリングが行われてしまう可能性が高い