🫧

Reactのレンダリングてどうなってんの?ていう話

2023/02/19に公開

概要

Reactを触っていて、レンダリングのトリガーと最適化の方法については意識しているが、正直Reactのレンダリングの仕組みについてはあまり考えることがなく、仮想DOMを生成しているんでしょ?くらいの知識でした。
Reactのレンダリング=ブラウザのレンダリングだと思っていましたが、実は違うみたい...
今回はReactのレンダリングについてざっとまとめてみようと思います。ほんとにざっとです。かなり深そうなので。

前提知識

DOMとは

DOM(Document Object Model)とはWebページの構造を表すオブジェクトのツリー構造のことでHTMLやXMLなどの文書を、プログラムから扱いやすくするために標準化されたものです。
DOMは、Webページ全体の構造をツリー状(木構造)に表現し、最上位の要素はdocumentとなっています。
例えば以下のようなHTMLの場合、

<html>
  <head>
    <title>HogeTitle</title>
  </head>
  <body>
    <h1>HogeH1</h1>
    <p>HogeP</p>
  </body>
</html>

DOMはこんな感じになります

document
  - html
    - head
      - title
        - "HogeTitle"
    - body
      - h1
        - "HogeH1"
      - p
        - "HogeP"

Reactのレンダリングのトリガー

Stateが更新された時、親コンポーネントがレンダリングされた時、Propsの値が変化した時などが挙げられます。memoやuseCallBackで最適化することができます。

Reactのレンダリング=ブラウザのレンダリングではない

僕が誤認識していたことですが、トリガー(stateの変化など)によって、レンダリングが発生した際、ブラウザがレンダリングされていると思っていましたが、正確にはReactのレンダリングとブラウザのレンダリングは別物であるらしいです。
いや別物というか最終的にはブラウザもレンダリングされることがありますが、Reactが指しているレンダリングとブラウザレンダリングは役割が違うというイメージです。

Reactのレンダリング

Reactのレンダリングのステップは大きく分けて以下のような順番になります。

1.Trigger
2.Render
3.Commit

Trigger

Triggerでまずレンダリングのトリガーを検知します。 トリガーとは初回レンダリングも含めて、再レンダリングが発生するあらゆるトリガーを検知します。

Render

ここがReactのレンダリングで一番重要なステップとなります。Renderとあるようにレンダリングをします。が...
このレンダリング(Render)という言葉が誤解を招きます。 ここで行っているレンダリングはブラウザのレンダリングではなく、コンポーネントを呼び出して、 大きく分けると以下のことを行っています。
1.仮想DOMの構築
2.レンダリングの比較
3.レンダリングの最適化

ここでもレンダリングというワードが出てきておかしくなりますが、ここでのレンダリングは、「コンポーネントを呼び出して状態を読むこと」 と僕は解釈しています。

そしてReactにおけるレンダリングとはこの三つのプロセスを行うことになります

①仮想DOMの構築

コンポーネントがレンダリング(呼び出し)されるとReactはそのコンポーネントの仮想DOMを構築します。
仮想DOMとは通常のDOMとは異なり、実態はReactが管理するJavaScriptのオブジェクトです。
例えば以下のような、コンポーネントがあるとします。

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount((preCount) => preCount + 1);
  }
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Countに加算</button>
    </div>
  );
}

このコンポーネントでReactが構築する仮想DOM(JSのオブジェクト)は多分以下のようになります。

{
  type: 'div',
  props: {
    children: [
      {
        type: 'p',
        props: {
          children: 'Count: 0'
        }
      },
      {
        type: 'button',
        props: {
          children: 'Countに加算',
          onClick: handleClick
        }
      }
    ]
  }
}
②レンダリングの比較

仮想DOMが構築されるとReactは前回(一個前)の状態と比較を行います。(diffをとる)
例えば「Countに加算」というボタンが押されてstateが更新されCountが1になった場合は仮想DOMは以下のように変更されます。

{
  type: 'div',
  props: {
    children: [
      {
        type: 'p',
        props: {
          children: 'Count: 1' // ← ここが変更された部分
        }
      },
      {
        type: 'button',
        props: {
          children: 'Countに加算',
          onClick: handleClick
        }
      }
    ]
  }
}

stateが変化し

children: 'Count: 0'

children: 'Count: 1'

となっていることを検知します。

③レンダリングの最適化

Reactはパフォーマンスの観点で不要なレンダリングを防ぐため(ここでのレンダリングはブラウザレンダリングと捉える)、もしstateやpropsなどが変化しておらず、②レンダリングの比較でdiffが検知されなかった場合、ブラウザのレンダリングを行いません。(Commitを行わない)

もし状態の変化が検知されると、Reactはその差分のみをCommitします。 このCommitがいわゆるブラウザレンダリングに当たります。

ここまでを整理するとReactのレンダリングとは、Reactのレンダリングは前回との状態の差分を比較して、ブラウザレンダリング(Commit)をするかを判定すること と言えます。

参考
React公式
Reactのレンダリングを理解する

Discussion