はじめまして!HRBrainでフロントエンドエンジニアをしている鈴木(@suzuesa)です
さて、早いものでHooksがリリースされて2ヶ月が経とうとしています
みなさんHooksを使いこなせてますか?私はまだまだ使いこなせません…
今回はその新しいHooksの機能をパフォーマンスチューニングの話と絡めてご紹介したいと思います
前提
パフォーマンスチューニングと言っても、どうしてReactが遅くなるのか、何処を改善すれば速くなるのかを知っておく必要があります
上の記事にすごい詳しく書いてあるので、そちらを見てからこの記事を読むともっと理解度が深まるかもしれません
簡潔に言えば 無駄な計算を抑え 、再レンダリングをできるだけ抑える この2つがポイントです
特に再レンダリングの負荷が高く、抑制する効果が高いためこれを重点的に考えていく必要があります
ReactはPropsもしくはStateが変更されたコンポーネント以下の すべてのコンポーネント を再レンダリングします
ですので子のコンポーネントで「このコンポーネントはいつ再レンダリングしないか」を定義し、不要な再レンダリングを回避していくホワイトリスト形式で各コンポーネントを調整していきます
これまでのチューニング方法
Hooksを使ったパフォーマンスチューニングのお話をする前に、これまでの方法をおさらいさせてください
既にパフォーマンスチューニングについて知見のある方はHooks以降のチューニングまで飛んでOKです
ClassComponent
ClassComponent では基本的に shouldComponentUpdate
メソッドをオーバーライドし boolean
を返すことでそのコンポーネントをいつ再レンダリングするかカスタマイズすることができました
class Component extends React.Component<{ text: string }> { shouldComponentUpdate(prevProps, nextProps) { const isTextDiff = prevProps.text !== nextProps.text return isTextDiff } }
しかし、これでは Props
を追加する度に比較式を書かなければならず、かなり面倒でした
これを解決するためReactの機能としてPureComponent
が発表され、自動 ShallowEqual
機能により格段にコード記述量が減り簡単かつ簡潔にパフォーマンスチューニングが可能になりました
class Component extends React.PureComponent<{ text: string }> { // shouldComponentUpdate を書かなくて良い }
FunctionComponent
ClassComponentでは shouldComponentUpdate
や PureComponent
を使って再レンダリングをコントロールすることができましたが、FunctionComponentはそういった機能が無く、必ず再レンダリングされてしまっていました
しばらく経ってReact v16.6で発表された memo
によりFunctionComponentでも shouldComponentUpdate
と同じことができるようになりました
// PureComponent相当 const Component = React.memo<{ text: string }>(({ text }) => { ... }) // shouldComponentUpdate相当 但し第二引数関数の返り値は再レンダリングする場合にtrueにする const Component = React.memo<{ text: string }>(({ text }) => { ... }, (prevProps, nextProps) => { const isTextEqual = prevProps.text === nextProps.text return isTextEqual })
Hooks以降のチューニング
みなさんはこうしてパフォーマンスチューニングをしてきと思いますが、Hooksの登場により更にパフォーマンスを極めることができるようになりました
キーとなるのは useMemo
と useCallback
の2つです
useMemo
一旦再レンダリングを抑える話は置いておいて、
useMemo は計算結果を記憶し、必要な時だけ再計算することができる機能で、例えば以下のような場合に有効です
const Component: React.FC = ({ products }) => { const soldoutProducts = products.filter(x => x.isSoldout === true) }
Propsから渡された products
から「売り切れの商品」を計算していますが、商品が10,000個程度あったとすると(こんなこと無いとは思いますが…)再レンダリングの度に計算するのは負荷がかかります
そんな時に useMemo
を使用するとPropsの products
が 変更された時のみ 計算させることが可能です
const Component: React.FC = ({ products }) => { - const soldoutProducts = products.filter(x => x.isSoldout === true) + const soldoutProducts = React.useMemo(() => products.filter(x => x.isSoldout === true), [products]) }
useMemo
は第一引数関数の結果を保持し第二引数で渡された値に変更があった時のみ再軽鎖されます
上の例だと products
が変更された時のみとなるので、いくら Component
が再レンダリングされようとPropsの products
が変更されない限り .filter()
関数は実行されません
個人的には useMemo
全部の変数に付けるくらいの勢いで書いてしまうのがおすすめです
memo() + useCallback()
useCallback
は子コンポーネントに渡すコールバック関数を記憶しておく事ができる機能です
単体ではあまり意味がない(らしい)ですが、前述の memo()
と併用することで一番の効果を発揮します
const App = () => { const [text, setText] = React.useState('') return ( <> <input type='text' value={text} onChange={e => setText(e.target.value)} /> <Wrap /> </> ) } const Wrap = () => { const [isChecked, setIsChecked] = React.useState(false) const toggleChecked = () => setIsChecked(!isChecked) return <Checkbox value={isChecked} onClick={toggleChecked} /> } const Checkbox = React.memo<{ value: boolean; onClick: () => void }>( ({ value, onClick }) => { console.log('Checkbox is renderd!') return ( <div style={{ cursor: 'pointer' }} onClick={onClick}> {value ? '☑' : '□'} </div> ) } )
ちょっと長くて説明し辛いので是非みなさんも自分の環境で動かしてみてください
注目して欲しいのは Checkbox
で、この状態だと memo
化されているにも関わらず App
-> Wrap
の順に再レンダリングがかかると Checkbox
も再レンダリングされてしまっています
これを useCallback
を使って以下のように修正します
... const Wrap = () => { const [isChecked, setIsChecked] = React.useState(false) - const toggleChecked = () => setIsChecked(!isChecked) + const toggleChecked = React.useCallback(() => setIsChecked(!isChecked), [isChecked]) return <Checkbox value={isChecked} onClick={toggleChecked} /> } ...
実際に実行して確認してみましょう
input
に変更があっても Checkbox
が再レンダリングされていないことがわかります
修正前再レンダリングされてしまっていた原因はコールバック関数にあり、Wrap
が再レンダリングされると toggleChecked
が 新しく 作り直され Checkbox
に渡されるので、 Checkbox
の ShallowEqual
はこれを「前回とは違うPropsだ!」と判断し再レンダリングしてしまうのです
useCallback
を使用して isChecked
が変更された時のみ作り直すとしたことで、 Wrap
が再レンダリングされた際には作り直されず記憶されるため ShallowEqual
で同じPropsだと判断され再レンダリングされないようにすることができます
useCallback
に関しても全部に付けるくらいの勢いで書くのがオススメです
(どこのコンポーネントがmemoしててどれをuseCallbackにしないといけなくて…と管理する方が大変)
実践
ここまで新しい useMemo
と useCallback
について解説してきましたが、習うより慣れろで練習問題を用意しました
お寿司のメニューを管理する簡単なアプリを作ってみました
今のままではかなりイケてないので、是非これをForkしてパフォーマンスを最強にしてみてください!