環境
- windows10
- DockerDesktop for Win 3.5.x
- node 16.13.1
- npm 8.1.2
- TypeScript 4.5
- React 17.0.1
- Next.js 12.0.7
- Recoil 0.5.2
- VsCode
- gitbash 2.32.0.1
パフォーマンスチューニング関連
Reactには、パフォーマンスを最適化させるための仕組みが用意されている。
レンダリングのタイミングと注意点
Reactでは、以下の場合にレンダリングが走る
- stateが更新された時
- propsが更新された時
- 親コンポーネントが再レンダリングされた時
複数のコンポーネントで構成されるReactアプリでは、レンダリングの制御を適切に行うことがアプリのパフォーマンスを向上させるうえで最も重要とされている
レンダリングを制御するための仕組み
以下のようなReactのHooksを使用して、無駄なレンダリングを防止することにより、パフォーマンスを向上させることが可能
https://ja.reactjs.org/docs/hooks-intro.html
- usecallback
- React.memo
- useMemo
React.memo
基本
- コンポーネント自体の不変値化のために使用される
- propsを受け取るコンポーネントであれば、そのコンポーネントの関心のあるpropsが更新された時のみ再レンダリングを行いたいという前提がある
- React.memoで対象のコンポーネントをラップするで不変値化が実現できる
- そのコンポーネントのレンダリングに要する時間が多ければ多いほど、メモ化の効果は大きい
- なんでもかんでもメモ化すればよいということではない。メモ化にもコストはかかるので、重たい処理などが記述されているコンポーネントはメモ化する等の線引きを決めておくのがよい
- propsの値に変更がなければ、レンダリングが生じず、キャッシュを利用する
使い方
親コンポーネント
import BasicHeadingOne from 'components/atomic/Atoms/Heading/BasicHeadingOne'
const Home = () => {
const [count, setCount] = useState(0)
return (
// メモ化されたコンポーネントA
<BasicHeadingOnetext={'トップページ'} />
// Aとは無関係のコンポーネント
<BasicButton
onClick={() => setCount((prevCount) => prevCount + 1)}
buttonName="カウントアップ"
/>
)
}
メモ化されたコンポーネントA
- レンダリングが走るとコンソールに出力されるようにしている
- memo化されているので、このコンポーネントが受け取るpropsが変更されない限り、レンダリングは走らず、ログにも出力されない
import React from 'react'
type Props = {
idAttribute?: string
className?: string
text: string
}
const BasicHeadingOne = (props: Props) => {
console.log('h1コンポーネントのレンダリングが走りました')
return (
<h1 id={props.idAttribute && props.idAttribute.toString()} className={props.className}>
{props.text}
</h1>
)
}
export default React.memo(BasicHeadingOne)
無関係なコンポーネント
- メモ化されたコンポーネントAとは無関係の親コンポーネントのstateを更新する
import React from 'react'
const BasicButton = (props) => {
return (
<Button bg={props.bg} className={props.className} onClick={props.onClick}>
{props.buttonName}
</Button>
)
}
export default BasicButton
React.memoでラップするコンポーネントに渡すpropsにオブジェクトが含まれる場合
- React.memoは第二引数にオブジェクトの比較(厳密な比較)用の関数を渡す必要がある(渡さないと、浅い比較(shallowEqual)が実行される。
- shallowEqualでは、常に異なるオブジェクトと判定されるので、等価性がないと判断され、必ず再レンダーされてしまう)
- 第二引数の関数(公式ドキュメントではこの関数のことをareEqualと呼んでいる)がtrueを返したときは再レンダーをスキップし、falseを返したときは再レンダーを行う。
import React, { useCallback, useState, useEffect } from 'react'
import Link from 'next/link'
type Href = {
pathname: string
query?: any
}
type Props = {
href: Href
name: string
className: string
}
const BasicLink = React.memo(
(props: Props) => {
const { href, name, className } = props
return (
<Link
href={{
pathname: href.pathname,
query: href.query
}}
>
<a className={className}>{name}</a>
</Link>
)
},
(prevProps: Props, nextProps: Props) => {
return prevProps.href === nextProps.href
}
)
export default BasicLink
useCallback
基本
- コールバック関数の不変値化のために使用される
- アロー関数は、実質的に中身が同じだったとしても毎回関数が評価され、新規の関数オブジェクトが生成される
- 上記のアロー関数の特性から、子コンポーネントをmemo化していても、アロー関数については、毎回propsが更新されていると判定され、レンダリングが走ってしまう
- 関数の評価の際にuseCallbackをくぐらせることで、その関数の同一性をチェックする。そして実質的に同一の関数であると判定されると、propsの値が変わらないので、結果として無駄なレンダリングが走らないという仕組み。
使い方
- 関数を更新したい場合の条件(依存関係)は、useCallbackの第二引数に指定する必要がある
- 以下の例では、体重と身長のinputが変わらない限り、関数は再生成されないようになり、メモ化されたBasicButtonコンポーネントも不要な再レンダリングを回避することが可能
import BasicButton from 'components/atomic/Atoms/Button/BasicButton'
const SampleComponent = () => {
const [weight, setWeight] = useState(0)
const [height, setHeight] = useState(0)
const computeBmi = useCallback(() => {
// ユーザーの入力値として体重・身長を受け取ってbmiを計算すると想定する
const bmi = weight / (height * 2)
console.log(bmi)
return bmi
}, [weight, height])
return (
...割愛
// propsでcomputeBmiを渡しているが、weightもしくは、heightのstateが変更されない限りはこの関数をpropsとして渡すことによる無駄なレンダリングは防止できる
<BasicButton onClick={computeBmi} buttonName="bmiを計算" />
useMemo
基本
- 関数の実行結果をメモ化するためのフック
- useEffect、useCallbackと同様、第二引数に依存関係を指定することで、その値に変更があった場合のみ処理が走る
- 空配列を渡すと何にも依存しないので、1回のみ実行
使い方
import { useMemo } from 'react'
const memoizedValue = useMemo(() => {
someThingFunction() // 何かしらの重たい処理
}, [])
まとめ
いかがでしたでしょうか。本記事では、全5部構成でReactの基礎知識や知っておくと便利なTIPSについて紹介しており、そのPart3の内容になります。対象者としてはReactの初学者~中級者を対象としており、Reactの基本的な書き方は知っているが、細かい処理や仕組みについては理解できていないという方には役に立つのではないかと思います