📚

仮想DOMについての振り返り

2021/02/05に公開

はじめに

初めまして!
近年、ReactやNext.jsといった、仮想DOMを用いて開発をされることが多くなってきました。
私自身も、これらを用いて開発をしていますが、レンダリングの流れやタイミングなど、DOMについて触れずに進めてしまっている点がありました。
そこで、今回は自身の知見を深めるために、仮想DOMについて調査し、まとめていきます。
間違い等は、指摘/コメントいただけると助かります!!

そもそもDOMとは?

DOMとは

仮想DOMに触れる前に、まずはDOMについて触れていきます。DOMとは、HTMLやXMLのような文書をプログラムを通して、直接変更できるインターフェイスのことですDOMの紹介 - Web API | MDN

<h1 id="title">Hello DOM</h1>

const title = document.getElementById("title");
title.innerHTML = "title are changed";
title.style.color = "red";

ここでは、getElementByIdを用いて、DOMから"title"のidを持つ要素を取得しています。取得した要素は直接innerHTMLで書き換えたり、styleで変更することも可能です。

レンダリングの仕組み

レンダリングの基本的な流れは、次の通りです。


ブラウザの仕組み: 最新ウェブブラウザの内部構造より引用

HTMLの解析

まず、ブラウザが要求されたコンテンツを画面に表示する際には、HTML等のドキュメントを解析し、レンダリングのエンジンの内部表現に変換します。

  • HTMLの読み込みの場合、解析したものをDOMツリーへと構築していきます。
    変換は以下の工程で進められており、画像やCSSなどの取得や読み込みも行います。
  1. 字句解析によるトークンのリスト化
  2. 構文解析による構文木構築
  3. 構文木内にあるJavascriptを実行し、DOMツリーを構築
  • 読み込まれたCSSは、レンダリングエンジンによって解析され、CSSOMツリーへと変換されます

レンダリングツリーの構築

ドキュメントを解析し、DOMツリーとCSSOMツリーへと構築したものを組み合わせて、レンダリングツリーを構築していきます。


レンダリング ツリーの構築、レイアウト、ペイントより引用

レンダリングツリーは、コンテンツを正しい順序で描画できるように管理しており、表示されるコンテンツの構造を示しています。レンダリングツリーが正しく構築されたら、レイアウト処理に進みます。

レイアウト処理

レイアウト処理では、構築したレンダリングツリーをもとに実際の位置とサイズの情報を加えていきます。親となるノードからレイアウトされ、再帰的に子ノードまでレイアウトされていきます。

描画

表示されるノードとスタイル、レイアウト等がわかったので、これらの情報をもとに画面上に実際に描画していきます。この段階で、実際のピクセルに変換されます。

このように、画面に実際に表示されるまでにはURLから要求されたHTMLなどを解析し、それぞれのツリーへと構築します。構築されたツリーに実際に表示する際の位置やサイズといった情報を付加し、描画しています。
これらのレンダリングの処理は、ユーザーからアクションや実行処理によってDOMイベントが発火する際に、再レンダリングの処理が発生します。

このように、レンダリングはブラウザにとってコストの高い処理です。
仮想DOMは無駄なレンダリング処理を減らして、レンダリングコストを小さくします。

仮想DOMとは?

仮想DOMとは、DOMをJavascriptのオブジェクトとして表現しているものです。DOMそのものを操作するのではなく、オブジェクトを変更して差分のみをDOMに反映することで、パフォーマンスを向上させます。
実際には、以下のような流れで動作しています。

  1. 仮想DOMツリーを2種類用意(変更前後の2種類)
  2. データの変更を検知
  3. 仮想DOMを再構築
  4. 変更前後の仮想DOMツリーを用いて差分検知する
  5. 差分があった箇所だけDOMに反映する

仮想DOMを用いることでレンダリングコストを低くできることの他に、

  • UIとロジックの分離
  • 状態の管理の簡略化
  • UIとロジックを繋ぐ処理が容易

といったメリットも得ることができます。
逆に、オブジェクトをしっかり管理しないと、パフォーマンスの低下につながったりします。
例えば、固有のkeyを降らなかった際に、不必要な箇所の再レンダリングが発生したりします。
どこで差分をとり、再レンダリングが発生しうるかを考えることが、パフォーマンスを向上、低下させないために必要になってきます。

ここで、実際にボタンをクリックする操作を行った際のコードの比較を見ていきます。

DOMを直接操作する場合

<div id="app">
  <p id="counter">0</p>
  <button type="button" id="increment">+1</button>    
</div>

<script>
const state = { count: 0 };
const btn = document.getElementById('increment');
btn.addEventListener('click', () => {
  const counter = document.getElementById('counter');
  counter.innerText = ++state.count;
})
</script>

上記のコードは、以下のような処理になっています。

  1. stateというオブジェクトで現在のcount数を管理(初期値0)
  2. buttonタグをidで要素取得('increment')
  3. ボタンがクリックされたとき、idが'counter'の要素を取得
  4. 取得した要素の文字をstate.countでインクリメント、更新する

この場合、UIとロジックが混在していたり、状態管理等がめんどくさい印象になっています。

仮想DOM(React)の場合

Counter.jsx
export const Counter = () => {
  const [count, {increment}] = useCounter();
  return (
    <div>
      <p id="counter">{count}</p>
      <button onClick={increment}>+1</button>
    </div>
  )
}
useCounter.js
export const useCounter = () => {
  const [counter, setCounter] = useState(0);
  const increment = () => {
    setCounter(counter+1);
  }
  return [counter, {increment}]
}

上記のコードでは、UIを担当する部分とロジックを担当する部分で分離しています。
このようにロジックをUIから分離することで、ロジックの使い回しが可能であったり、最終的なViewの状態がわかりやすかったり、状態を管理しやすくなります。

まとめ

DOMと仮想DOMについて、まとめました。
仮想DOMは従来のDOMと比較し、レンダリングコストを低減しやすく、開発する上でUIとロジックの分離がしやすいものだとわかります。
これらは、レンダリングのパフォーマンスを向上させるものと同時に、正しく扱わなければ逆にパフォーマンスが低下してしまうこともわかりました。
この辺りを意識して、これからも開発をしていきたいと思います。

参考記事

ブラウザの仕組み: 最新ウェブブラウザの内部構造
仮想DOMは本当に“速い”のか? DOM操作の新しい考え方を、フレームワークを実装して理解しよう
ブラウザレンダリングの仕組み

GitHubで編集を提案

Discussion