😎

Reactのレンダリングを最適化する

2023/09/09に公開

この記事では、ReactやNext.jsにおけるレンダリングの最適化を行うための技を教えます。

ReactやNext.jsにおいて1つのコンポーネント内に複数の子コンポーネントを持つことはよくあります。また、親のコンポーネントがレンダリングされると、小コンポーネントもレンダリングされるというのが仕組みです。このレンダリングというのが、アプリケーションにおいてパフォーマンスに影響を与えてしまします。

本来レンダリングしなくてもいいコンポーネントたでレンダリングされてしまうと、仮に重たい処理をどこかのコンポーネントが担っていたときに、別のコンポーネントに影響を与えてしまい、結果としてアプリケーション全体のパフォーマンス低下を引き起こしてしまいます。

このレンダリングの制御をするためにReactが用意している方法を紹介します。

今回紹介するのは以下の3つです。

  • React.memo
  • useMemo
  • useCallback

React.memo

概要

このReact.memoはコンポーネント全体をラップすることができます。例えば、親のコンポーネントがレンダリングされたとしても、この子コンポーネントはレンダリングされなくなるため、コンポーネント自体をレンダリングさせたくないときに有効です。

使い方

'use client'

import Child from "@/components/Child"
import React, { useState } from "react"

export default function Parent() {
  const [text, setText] = useState('')

  return (
    <div className="border w-96 h-80 mx-auto mt-24 p-12 bg-slate-300">
      <p>親コンポーネントです</p>
      <input
        type="text"
        value={text}
        onChange={handleChange}
        className="border border-black"
      />
      
      <Child />
    </div>
  )
}
import React from 'react'

// eslint-disable-next-line react/display-name
const Child: React.FC = React.memo(() => {
  return (
    <div className='border border-red-600 p-4 mt-2 bg-red-300'>
      <p>小コンポーネントです</p>
    </div>
  )
})

export default Child

この場合、上のコードが親コンポーネント、下が小コンポーネントになります。このとき

親コンポーネントではconst [text, setText] = useState('') のようにstateが定義されています。このtextの値が変化するたびに、親コンポーネントのレンダリングが発生します。

このときに、親コンポーネント内でインポートされた<Child />コンポーネントも同時に際レンダリングされることになります。しかし、今回は、小コンポーネントが際レンダリングされていることは、意味をなしていません。そんなときにメモ化をすることで、際レンダリングを解消することができます。

上記で記載しているように、React.memo()というようにしてあげることで、メモカを実現できます。

useMemo

概要

useMemoは変数におけるメモ化の役割を果たしてくれます。例えば、先ほどの小コンポーネントにおいて、コンポーネント自体のメモ化はしなくてもいいが、ある一個の処理が非常に時間がかかり、かつ親コンポーネントのレンダリングと干渉していない場合、変数をメモ化し、パフォーマンスを向上させられます。

使い方

'use client'
import React, { useMemo, useState } from 'react'

// eslint-disable-next-line react/display-name
const Child: React.FC = () => {
  const [count, setCount] = useState(0)
	const double = (count: number): number => {
    let i = 0
    while(i < 1000000000) i++;
	    return count * 2
	  }

  const doubleCount: number = useMemo(() => double(count), [count])
  console.log(doubleCount)
  return (
    <div className='border border-red-600 p-4 mt-2 bg-red-300'>
      <p>小コンポーネントです</p>
			
			<p>重い処理</p>
      <p>Counter: {count}, {doubleCount}</p>
      <button
        onClick={() => setCount(count + 1)}
        className="border border-black rounded-sm px-2 bg-slate-50"
      >
        ボタン
      </button>
    </div>
  )
}

export default Child

この場合、小コンポーネントはReact.memoでラップされていないため、親コンポーネントがレンダリングされたときに、小コンポーネントも同時に実行されるようになります。

しかし、小コンポーネントでは重い処理が走るようになっています。例えば、親コンポーネントのinput内に文字を入力するたびに、この重い処理が走ることになります。そうすると、文字入力に支障が出ます。これは、ユーザ体験が悪いアプリケーションになってしまいます。

これを回避するために、useMemoというフックスを使います。このフックスを用いることで、コンポーネントがレンダリングする際に、ラップされた処理をスキップしてくれます。これにより、レンダリング時に実行したくない処理をメモ化することができました。しかし、いつ何時も処理の実行が行われないというのはおかしいですよね。useMemoは第二引数に依存配列を記載することができます。これによってどの値が変化したときにこの思い処理を実行するのかを決定することができます。例えば今回のコードで言えば、countが変更された時のみ重い処理が走るということです。

useCallback

概要

関数のメモ化をすることができる。基本的には、useMemoと同じです。親コンポーネントに定義している関数を小コンポーネントにpropsで渡しているとしましょう。この場合、その関数と関係のないところで親コンポーネントがレンダリングされた場合、小コンポーネントも同時にレンダリングされることになります。このとき、その処理をスキップしてくれるのがuseCallbackです。

使い方

'use client'

import Child from "@/components/Child"
import Child2 from "@/components/Child2"
import React, { useCallback, useState } from "react"

export default function Parent() {
  const [text, setText] = useState('')
  const handleChange = (e: any) => {
    setText(e.target.value)
  }

  const handleClick = useCallback(() => {
    console.log('click!')
  }, [])

  return (
    <div className="border w-96 h-80 mx-auto mt-24 p-12 bg-slate-300">
      <p>親コンポーネントです</p>
      <input
        type="text"
        value={text}
        onChange={handleChange}
        className="border border-black"
      />
      
      <Child />
      <Child2 handleClick={handleClick} />
    </div>
  )
}
import React from 'react'

// eslint-disable-next-line react/display-name
const Child2: React.FC<any> = React.memo(({ handleClick }) => {
  return (
    <div className='border border-red-600 p-4 mt-2 bg-red-300'>
      <button
        className="border border-black rounded-sm px-2 bg-slate-50"
        onClick={handleClick}
      >
        Print Click!
      </button>
    </div>
  )
})

export default Child2

上のような状況の場合、小コンポーネントは、React.memoでメモ化されている為、親コンポーネントがレンダリングされても、小コンポーネントはレンダリングされませんが、propsとして、handleClickという関数を渡しているため、親コンポーネントがレンダリングされるたびに、handleClickが生成されその度に、小コンポーネントもレンダリングされることになります。

これを回避するために、const handleClick = useCallback(() => {console.log('click!')}, []) このようにuseCallbackで囲んであげることで、処理をスキップし、小コンポーネントの無駄なレンダリングを回避することができます。

まとめ

今回紹介した内容はアプリケーションのパフォマーンさを向上するのに役に立ちます。ただし、多用するものではないということを覚えておきましょう。必要な時や、重い処理でどうしても使わないといけない時に使いましょう。

アプリケーションが複雑になればなるほど本来レンダリングしないといけないコンポーネントや、スキップしてはいけない処理など区別が難しくなります。あくまでも最終手段的な考えでいいかと思います。

Discussion