仮想DOMについての振り返り
はじめに
初めまして!
近年、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などの取得や読み込みも行います。
- 字句解析によるトークンのリスト化
- 構文解析による構文木構築
- 構文木内にあるJavascriptを実行し、DOMツリーを構築
- 読み込まれたCSSは、レンダリングエンジンによって解析され、CSSOMツリーへと変換されます
レンダリングツリーの構築
ドキュメントを解析し、DOMツリーとCSSOMツリーへと構築したものを組み合わせて、レンダリングツリーを構築していきます。
レンダリングツリーは、コンテンツを正しい順序で描画できるように管理しており、表示されるコンテンツの構造を示しています。レンダリングツリーが正しく構築されたら、レイアウト処理に進みます。
レイアウト処理
レイアウト処理では、構築したレンダリングツリーをもとに実際の位置とサイズの情報を加えていきます。親となるノードからレイアウトされ、再帰的に子ノードまでレイアウトされていきます。
描画
表示されるノードとスタイル、レイアウト等がわかったので、これらの情報をもとに画面上に実際に描画していきます。この段階で、実際のピクセルに変換されます。
このように、画面に実際に表示されるまでにはURLから要求されたHTMLなどを解析し、それぞれのツリーへと構築します。構築されたツリーに実際に表示する際の位置やサイズといった情報を付加し、描画しています。
これらのレンダリングの処理は、ユーザーからアクションや実行処理によってDOMイベントが発火する際に、再レンダリングの処理が発生します。
このように、レンダリングはブラウザにとってコストの高い処理です。
仮想DOMは無駄なレンダリング処理を減らして、レンダリングコストを小さくします。
仮想DOMとは?
仮想DOMとは、DOMをJavascriptのオブジェクトとして表現しているものです。DOMそのものを操作するのではなく、オブジェクトを変更して差分のみをDOMに反映することで、パフォーマンスを向上させます。
実際には、以下のような流れで動作しています。
- 仮想DOMツリーを2種類用意(変更前後の2種類)
- データの変更を検知
- 仮想DOMを再構築
- 変更前後の仮想DOMツリーを用いて差分検知する
- 差分があった箇所だけ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>
上記のコードは、以下のような処理になっています。
- stateというオブジェクトで現在のcount数を管理(初期値0)
- buttonタグをidで要素取得('increment')
- ボタンがクリックされたとき、idが'counter'の要素を取得
- 取得した要素の文字をstate.countでインクリメント、更新する
この場合、UIとロジックが混在していたり、状態管理等がめんどくさい印象になっています。
仮想DOM(React)の場合
export const Counter = () => {
const [count, {increment}] = useCounter();
return (
<div>
<p id="counter">{count}</p>
<button onClick={increment}>+1</button>
</div>
)
}
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操作の新しい考え方を、フレームワークを実装して理解しよう
ブラウザレンダリングの仕組み
Discussion