5歳娘「パパのReact、めっちゃ遅いね!」


新しい記事もよろしくやで!
ハスケル子「タグごとに色がついてたらいいのにな…」

38歳無職ワイ

ワイ「(カタカタカタカタ・・・ッターン!)」

娘(5歳)「パパ、今日は何してるの?」

ワイ「今日はな、むかしWordPressで作った自分用TODOリストの」
ワイ「デザインをリニューアルしてんねん」

よめ太郎「(そんなことより職を探せや)」

娘「へぇ〜」
娘「WordPressってことは、PHPを書いてるの?」

ワイ「いや、ちゃうで」
ワイ「リニューアル後は、フロント部分をReactで実装しようと思ってな」
ワイ「そこで、WordPressをREST APIモードで使うことにしたんや」
ワイ「つまり、WordPressを管理画面つきAPIみたいに使うってことや」

娘「要は、WordPressをヘッドレスCMSとして使うんだね」

ワイ「ヘッドレス・・・?」
ワイ「ちゃうちゃう、管理画面つきAPIや」

娘「・・・まぁいいや」
娘「とにかく、APIからTODOリストのデータを取得して」
娘「それをReactで表示したいわけだね」

ワイ「そういうことや」

現在の進捗状況

娘「それで、フロントの実装はどこまでできたの?」

ワイ「ほぼ完成してるで」
ワイ「↓こんな感じや」

よめ太郎「(酒ばっかりやないかい・・・)」

ワイ「だいぶ下のほうに『職を探す』っていうタスクも」
ワイ「ちゃんと入ってるで!」

よめ太郎「(ちゃんととは・・・)」

娘「へぇ〜」
娘「ちょっと触ってみてもいい?」

ワイ「もちろんええで」

娘「(ポチポチポチ・・・)」
娘「う〜ん・・・」

ワイ「え、どしたん」

娘「パパのReact、めっちゃ遅いね!」

ワイ「え、マジ・・・?」

娘「なんか動きが重いよ?」

ワイ「え、そんなことないやろ・・・」
ワイ「(ポチポチポチ・・・)」
ワイ「あれ、ほんまや」
ワイ「検索フォームにタスク名を入力するときに、なんかモタついてんな」
ワイ「キーを叩いてから文字が表示されるまでの間に、タイムラグが1秒近くあるで」
ワイ「何やこれ・・・」

娘「パパ、もしかしたらコンポーネントたちが無駄に再レンダーされてるのかもよ」
娘「そんな時は、React Developer Toolsを使って再レンダー状況を可視化してみようよ」

ワイ「そ、そんな機能あんの?」

娘「あるよ」

  1. ChromeにReact Developer Toolsという拡張機能を追加する
  2. Chrome Devtoolsを開く
  3. Componentsタブを開く
  4. 歯車アイコンをクリック
  5. Highlight updates when components render.にチェックを入れる

娘「手順としては↑こんな感じだね」

ワイ「なるほど・・・」

ワイ「↑ここにチェックを入れるんやな!」

娘「そうだね!」

さっそくレンダー状況を見てみる

ワイ「何やこれ」
ワイ「タスク名の入力フォームに文字を1つ入力するたびに
ワイ「100件くらいあるTODOリスト全体が再レンダーされてるやん」
ワイ「まだ検索ボタンを押してもないのに・・・!」

娘「そうだね・・・」

ワイ「検索ボタンを押して、そんで検索結果が変わったことでリストが再レンダーされるならまだしも」
ワイ「文字を1つ入力したり消したりするたびにリスト全体が再レンダーされるんかい」
ワイ「何やそれ・・・」

娘「たぶん、タスク名のinputタグにイベントハンドラが設定されてて」
娘「文字を入力するたびに何かページの状態が変わって」
娘「それでページが再レンダーされてるんじゃない?」
娘「ページ内でuseStateとか使ってるよね?」

ワイ「おお、それはその通りや」

src/pages/TodoListPage.tsx
  const [taskName, setTaskName] = useState('')

ワイ「↑この、useStateで作ったsetTaskNameっていう関数があってな」
ワイ「その関数が、タスク名の入力フォームに文字を入力するたびに実行される仕組みやねん」

娘「なるほどね」
娘「その時に、TODOリストも毎回ぜんぶ再レンダーされちゃってるんだね」

React.memoで、コンポーネントをメモ化しよう

娘「パパ、そんな時はReact.memoを使って」
娘「TODOリストのコンポーネントをメモ化してみようよ」

ワイ「ど、どうやんの・・・?」

娘「ええと・・・」

src/components/TodoList.tsx
- const TodoList: React.FC<Props> = ({ list }) => {
+ const TodoList: React.FC<Props> = React.memo(({ list }) => {
    return (
      <ul>
        {list.map((task) => (
          <TodoItem task={task} key={task.id} />
        ))}
      </ul>
    )
- }
+ })

+ TodoList.displayName = 'TodoList'

娘「↑こうだね」

ワイ「おお、React.memoっていう関数に」
ワイ「TodoListコンポーネントの中身をぶち込んであげるだけなんやな」

娘「うん!」

よめ太郎「(前々から思ってたけど)」
よめ太郎「(引数をぶち込むって何やねん・・・)」

モタつきは解消されたのか

ワイ「おお、TODOリストがピカピカせんようになっとるわ」
ワイ「文字の入力が重たい感じもなくなっとる」

娘「よかったね!」

ワイ「・・・これはどういうことなん?」

娘ちゃんによる解説

娘「えっとね、今までは」

React「お、検索フォームに文字が入力されたで!」
React「ほな、ページの状態が変わったことやし、再レンダーや!」
React「TodoListコンポーネントも再レンダーや!」

娘「↑こんな感じだったんだけど」
娘「コンポーネントをメモ化すると・・・」

React「お、検索フォームに文字が入力されたで!」
React「でも、TODOリストの中身は変わってないから」
React「TodoListコンポーネントはレンダーし直す必要ないか・・・」
React「さっきメモしといた結果を再利用しとこか」

娘「↑こんな感じだね」

よめ太郎「(なんでReactも関西弁やねん)」

ワイ「なるほどなぁ」
ワイ「useStateで管理している状態が、文字入力によって変わったとしても」
ワイ「TODOリストに関係のない変更だけが起こっていた場合には」
ワイ「再レンダーをスキップしてくれるんやね」

娘「そういうことだね!」

ワイ「ありがとうやで、娘ちゃん!」

しかし30分後・・・

ワイ「こ、今度はタスクの削除機能を実装したら」
ワイ「また画面の動きが遅くなってもうた・・・」

娘「パパ、今度はどうしたの?」

ワイ「おお、娘ちゃん・・・」
ワイ「あのな・・・」

src/pages/TodoListPage.tsx
+ const onDelete = (taskId: number): void => {
+   /* 省略 */
+ }

  return (
    <>
      <h1>TODOリスト</h1>
      <form>
        {/* 省略 */}
      </form>
-     <TodoList list={todoList} />
+     <TodoList list={todoList} onDelete={onDelete} />
    </>
  )

ワイ「↑こんな感じで、タスク削除用の関数を作って」
ワイ「それをTodoListコンポーネントに渡すようにしただけなんや・・・」
ワイ「それなのに・・・」

ワイ「また文字入力のたびにTODOリスト全体がピカピカしてんねん・・・」

娘「そんな時は、またメモ化だね!

useCallbackで、関数もメモ化しよう

src/pages/TodoListPage.tsx
-  const onDelete = (taskId: number): void => {
+  const onDelete = useCallback((taskId: number): void => {
     setTodoList((prevList) => {
       return prevList.filter((todoItem) => todoItem.id !== taskId)
     })
-  }
+  }, [])

ワイ「おお、今度はonDelete関数の中身を」
ワイ「useCallbackというやつにぶち込んだるわけか」

娘「そうそう」
娘「これでもう一度、画面を触ってみて?」

ワイ「おお、チカチカしないし、サクサク入力できとるわ」
ワイ「・・・でも、これ何でなん?」
ワイ「TodoListコンポーネントに渡すonDelete関数は、毎回おんなじ関数なんやから」
ワイ「TodoListは再レンダーされないはずちゃうの・・・?」

娘「それはね・・・」

src/pages/TodoListPage.tsx
  const onDelete = (taskId: number): void => {
    /* 省略 */
  }

  return (
    <>
      <h1>TODOリスト</h1>
      <form>
        {/* 省略 */}
      </form>
      <TodoList list={todoList} onDelete={onDelete} />
    </>
  )

娘「↑ここで、onDelete関数を作って」
娘「TodoListコンポーネントに渡してるでしょ?」
娘「そこが問題・・・というかポイントなの」
娘「つまり・・・」

React「よっしゃ、onDelete関数を作って」
React「TodoListコンポーネントにぶち込むでぇ!」

娘「↑こんな感じで、作りたての新しい関数TodoListコンポーネントに渡す」
娘「そういうことになっちゃうの」

ワイ「作りたて・・・」
ワイ「そう言われてみれば、アロー関数式で関数を作って
ワイ「それをTodoListコンポーネントに渡してるわけやもんな」

娘「そう」
娘「だから・・・」

React「さっきまでとは違う、今作ったばかりのonDelete関数を渡すから」
React「TodoListコンポーネントも再レンダーや!」

娘「↑こんな感じになっちやうの」

ワイ「毎回新しい関数が作られて、別物扱いになってまうわけか」
ワイ「つまり、ReactはObject.isによる比較アルゴリズムを使用しているわけやな・・・」

よめ太郎「(いや、そこは急に理解早いんかい)」

娘「そういうことだね」
娘「そして、useCallbackを使うと」

ワイ「この関数は毎回作り直さないで、前回と同じのを使ってな!」

娘「ってことをReactに伝えられるわけだね」

ワイ「ふーん、そうするとTodoListコンポーネントが再レンダーされなくなるん?」

娘「そうだよ」

React「TODOリストの内容もonDelete関数も、前回と変わってへんな」
React「ほな今回はTodoListコンポーネントの再レンダーはスキップや!」

娘「↑って感じになるの」

ワイ「なるほどなぁ」
ワイ「ちなみにuseCallbackの第二引数の配列、これは何なん?」

娘「これは・・・」

ワイ「hogeっていう変数の中身が変わった場合には」
ワイ「前回と同じ関数を使いまわさずに、新しく作ってな!」

娘「↑的なことをReactに伝えるときのための配列だね」

ワイ「ほぇ〜」
ワイ「そうやって新しく関数が作り直された場合には」
ワイ「その関数を受け取ったコンポーネントは再レンダーされるわけか」

娘「そうそう」

ちなみに

娘「ちなみに、useCallbackを使わないでも」
娘「ページコンポーネントの外で関数を定義するだけで」
娘「再レンダーを抑止できる場合もあるよ!」

ワイ「そうかそうか」
ワイ「ページコンポーネントの外側で定義された関数は」
ワイ「ページが再レンダーされても変わらへんわけやな」
ワイ「まぁ、とにかく激重サイトにならんくてよかったわ!」

まとめ

  • Reactアプリの動作が重い時は、React Developer Toolsで再レンダー状況をチェックしてみよう
  • React.memoを使ってコンポーネントをメモ化し、無駄な再レンダーを抑止しよう
  • メモ化したコンポーネントに渡すための関数もuseCallbackでメモ化しておこう

ワイ「↑ってことやな!」

娘「そうだね!」

ワイ「いやー、娘ちゃんのおかげで良いTODOリストができそうやわ・・・!」

娘「あっ!」
娘「・・・パパ・・・!」

ワイ「なんや?」

娘「大変なことに気づいたんだけど・・・」

娘「このTODOリスト、どこからタスクを追加するの?」

ワイ「あ・・・」
ワイ「タスクの作成機能を作るの、忘れてたわ・・・!」

娘「・・・」

ワイ「で、でも大丈夫や!」

  • タスク追加機能を実装する

ワイ「↑っていうタスクを、このTODOリストに追加しておけばええねん!」
ワイ「そしたらもう忘れへん!」

娘「で、そのタスクはどこから追加するの?

ワイ「それな

〜おしまい〜

参考文献

新しい記事もよろしくやで!

ハスケル子「タグごとに色がついてたらいいのにな…」

補足