sunabox

仮想DOMを学び直す

仮想DOMは速いという漠然とした認識が自分の中にあったんだけど、じゃあなんで速いんだろう?そもそも本当に速いのか?って疑問に思った時に色々調べ直した

仮想DOMについてはすでに記事がいくつもあるので今回の記事と重複している部分は大いにあるが、自分の中で腑に落ちないこととか深く理解できたことがたくさんあったのでまとめておく

結論、仮想DOMはそもそも単純にパフォーマンスを速くするための文脈で生まれた技術ではなく、高いDXを実現しつつパフォーマンスを最適化してくれるための技術という認識でいる

この記事では仮想DOM誕生前の課題と仮想DOMによってそれがどのように解決されたのかをまとめる。
その過程で仮想DOMは本当に速いのかについても自分なりの答えが見えてきたのでそれについても言及する

色々調べた上で書いていますが、間違っているところがあれば教えてくださると嬉しいです!

仮想DOMの正体と差分検知アルゴリズム

まずはじめに仮想DOMの正体とどうやって差分検知をしているかをかなりざっくりとだけおさらい
正式な言い方はわからないが、この記事ではブラウザが持つDOMのことを仮想DOMと対比させてリアルDOMと表現することにする

仮想DOMはリアルDOMを表現するためのJSの軽量なオブジェクト
実際には以下のような感じになっており、children部分にこの構造が再起的に入ることによってツリー構造を持つようになっている

{
  "nodeName": "div",
  "attributes": { "id": "app" },
  "children": [...],
}

軽量なオブジェクトってのがポイントで、リアルDOMはいろんなプロパティを持っているが仮想DOMは差分検知に必要最低限なプロパティのみを持つことで、高速な差分検出処理を行うことができる

差分検知

仮想DOM構造体はメモリ上に保持されている
UIに変更が加えられた場合は変更後の仮想DOM構造体を作成し、メモリ上にある変更前の仮想DOM構造体と比較を行って、異なる部分だけを抽出してくる

この比較はブラウザ上である必要がなく、JSオブジェクトの比較なので比較的高速に処理を行える
最終的にここで検出された差分がリアルDOMに適用される

仮想DOMは本当に速いのか?

ここからが本題。
軽量なJSオブジェクトによる差分検知という方法で高速に差分検出を行うことができる仮想DOMだが、いくつかのブログや記事で「仮想DOMは変更する差分だけを検知してリアルDOMに反映させるので早い」みたいな説明をたびたび見る

最初はそうなんだとなんとなく納得していたんだが、次第に違和感を感じるようになり、それについて調べる前にそもそもブラウザのレンダリングからちゃんと学んだ方がいいなってなって勉強しなおした
その時の内容は以下に簡単にまとめてある

ブラウザレンダリングのしくみ | sunaboxブラウザがレンダリングを行う仕組みについて簡単にまとめた。
faviconsuna.dev

ブラウザのレンダリングについて学んだ後にもう一度、「仮想DOMは変更する差分だけを検知してリアルDOMに反映させるので早い」について考えると、そのまま飲み込むにはやっぱり違和感があった

仮想DOMを使ったとしても最終的には変更分をリアルDOMに反映させる必要がある
じゃあjQueryでその変更分をリアルDOMで直接変更した方が早いんじゃないか?
(以降、この記事ではリアルDOMを操作するものとしてjQueryを使う前提とする)

つまり、仮想DOM自体が高速なのは納得だが、ブラウザでレンダリングするまでの速さをリアルDOMと比較した時に本当に速いのか?
差分検知するという部分があるせいでむしろ遅くなりさえするのではないかと思った

(jQuery): イベント → 部分的なリアルDOMの変更 → ブラウザによるレンダリング
(React) : イベント → 仮想DOMによる差分検知と部分的なリアルDOMの変更 → ブラウザによるレンダリング

実際にはjQueryによるリアルDOMの変更と仮想DOMによるリアルDOMの変更ではアプローチの仕方が異なる可能性があるので、一概に比較はできなさそう。
jQueryでは対象のDOMの取得→変更というステップだが、仮想DOMでは実際にどうやってリアルDOMを変更してるかまではわからなかった

ただ、このリアルDOMの変更部分に関してはjQueryでも仮想DOMでもあまり差はないような気がする。
であるならば、「仮想DOMによる差分検知の部分がオーバーヘッドになってむしろ遅くなる可能性はある」という認識は正しいように思う。
(計測すれば正確なデータを出せそうだがやっていない。ツリー構造や規模によっても変わってきそう)

この辺は調査しきれなくて一部推測も混じってきてしまったが、自分なりには現時点では上記のような結論になった
じゃあ仮想DOMが特別レンダリングのパフォーマンスを速くするためのものではないならなんのためにあるのか?

ここからはその秘密を紐解いていく
キーワードは命令的な処理宣言的UI

命令的な処理の苦しみと宣言的UIという救世主

自分は本格的なフロントエンドの開発はReactから入った人間で、jQueryは使ったことはあるがそれによってゴリゴリ開発していくということをしたことがない
ただ、それでも開発体験はよくなかったし大規模な開発だとかなり苦しいだろうなーというのは想像に難くない

その主たる要因としてjQueryによるUIの変更は命令的な処理だからというのがある

先に結論を述べると、この煩雑な記述方法から解放するための手法が宣言的UIで、これをパフォーマンスを両立させた状態で可能にするというのが仮想DOMの主要な役割の1つである。

それについてまとめるために、まずはそれまで命令的な処理がどのように行われていたのかをおさらいしていく。

命令的な処理

イベントが起こった時にそれによって状態が更新される。
命令的な処理では状態が更新されるたびに、「その時の状態に基づいて何を行うか」を書く必要がある。

したがって最終的なUIを想像するためには、それぞれの状態の時間的な遷移とそのたびに何が行われたかを追う必要がある。

どういうことか具体例を用いる。
入力フォームがあって、文字数が10文字以上だとボタンがdisabledになる場合を考えてみる。
jQueryだと以下のような処理の流れになる

// 入力のイベント発火を検知した後
1. フォームに入力されている文字数を計算
2. ボタンの要素をgetElementByIdなどで取得する
3. 文字数が10文字以上だった場合、ボタンの”disabled”をtrueにする
4. 文字数が10文字未満だった場合、ボタンの”disabled”をfalseにする

このような〇〇して、〇〇して、〇〇してという処理を逐次的に記述するのが命令的処理。
これだとボタンが最終的にどうなるかはこの逐次的な処理を時系列的に追った上で想像することになる。

以下のコードの部分だけ見ても、このボタンがdisabledになる可能性があるかどうかはここだけでは判断できず、どういう場合にdisabledになるかはロジック部分を読みとかないといけない

<button>命令的ボタン</button>

これくらいの規模ならまだいいが、条件が複雑になっていくと時系列で処理を追いかけるのはかなり複雑になってくる。

この命令的なコードは上記のように読み解くのも大変であるが、書く際にも中々大変である。
欲しいUIに対してどういう操作を行なっていけばそうなるかを穴埋めしながら書かなければいけないので、欲しいUIと書くべきコードの間に乖離がある。

宣言的UI

命令的な記述で見たように、ボタンクリックやAPIでのデータ取得など様々な要因で遷移していくクライアントの状態に応じて、「何を行うか」によって最終的な状態が規定されるのはかなり辛い

それに対して、その時の状態に応じて一意なUIを描画するのが宣言的UIという手法
UIはどのようにその状態が更新されるかに関しては関心を持たず、常にその時々で与えられた状態に基づいてのみ構築される。

先程のボタンのdisabledの例をReactで表記すると以下のようになる

<Button disabled={inputValue.length >= 10 || !isChecked}>
  宣言的UIのボタン
</Button>

inputValueやisCheckedがどのような状態の時にdisabledになるのかがパッと見てわかる

このように「この状態ではこのようなUIになる」ということをあらかじめ記述することができるので、コードを書く際にも最終的なUIの形をコードに落とし込めるし、読む際にもどういう状態の時にどういうUIになるのかが把握しやすい。

宣言的UIと仮想DOM

さて、仮想DOMに話を戻す

前述したように宣言的UIによって、その時点でのデータに基づいた一意なViewが出力されるようになった。
仮想DOMを使うとリアルDOMを明示的に操作することなく、「変更された状態に基づいて構築されたDOM構造」と「変更前のDOM構造」を勝手に比較してくれてその差分だけがリアルDOMに反映される。

必然的にリアルDOMの更新部分は最小限に抑えられ、開発者はどの部分をどのように変更すべきかを意識することがなくなった。

仮想DOM登場以前にも、AngularJSなどではデータに基づいてUIを構築するというアプローチはあったらしい。
詳細は調査しきれていないが、こういったものはそのデータをバインドするアルゴリズムにパフォーマンス上の課題を抱えていたりして、中々普及しなかったとかなんとか。

一方、仮想DOMを使った実装方法では宣言的UIによる高いDXを実現しつつ、パフォーマンスの最適化をしてくれるようになった!

これが両立できるようになったことが仮想DOMによるパラダイムシフトだった

ここでこの記事を書くにあったっての最初の問題提起、仮想DOMは本当に速いのかという論点に関して振り返ってみる。

序盤で記述したが、実際単純にパフォーマンスという点だけで見れば、jQueryなどでリアルDOMを直接操作した方が速いケースもあると思っている。

仮想DOMは宣言的UIとパフォーマンス最適化を両立できるようになったところが肝であり、パフォーマンスの観点で言うとそれまでのリアルDOMを直接操作するのと比較して速いというよりも、開発者が意識しない形でパフォーマンスを最適化してくれて速い

認識が間違っていたら教えてもらえると嬉しいです!

まとめ

こうみると仮想DOMとReactって本当にゲームチェンジャーだったんだなーと感じた

仮想DOMはリアルDOMと1:1対応することが前提なので、リアルDOMによる操作は副作用であり、jQueryなどのリアルDOMを操作するライブラリと混在することはかなり難しくなる

めっちゃ余談だが、バックエンドをPHPからGoにリプレイスするリプレイス案件は経験したことがあってそれはそれで大変だったが、jQueryからReactへの段階的なリプレイスってその比じゃないくらい大変そうだなーと想像した。けどちょっとだけならやってみたい。ちょっとだけなら。笑

今回仮想DOMについて調べる中でそれまでの課題をどうやって解決したのかを深く理解できたと思う
Reactの思想的なところがますます好きになった

最近だと仮想DOMを使わない技術として代表的なものにSvelteがあるけど、長くなるのでそれについてはまた別記事でまとめようと思う

参考

なぜ仮想DOMという概念が俺達の魂を震えさせるのか - Qiita追記: 情報が色々と古くなったため、2020年に書き直した版へのリンクを張っておきます。https://zenn.dev/mizchi/books/0c55c230f5cc754c38b9この記…
faviconqiita.com
Optimizing React: Virtual DOM explained—Martian Chronicles, Evil Martians’ team blogLearn about React's Virtual DOM and use this knowledge to speed up your applications. In this thorough beginner-friendly introduction to framework's internals, we will demystify JSX, show you how React makes rendering decisions, explain how to find bottlenecks, and share some tips to avoid common mistakes.
faviconevilmartians.com
宣言的UI宣言的UIの状態管理とアーキテクチャSwiftUIとGraphQLによる実践 https://speakerdeck.com/sonatard/swiftui-graphql
faviconspeakerdeck.com
仮想DOMは本当に“速い”のか? DOM操作の新しい考え方を、フレームワークを実装して理解しよう|ハイクラス転職・求人情報サイト AMBI(アンビ)仮想DOMは本当に“速い”のか? DOM操作の新しい考え方を、フレームワークを実装して理解しようページです。AMBI(アンビ)は若手ハイキャリア向け転職サイト。年収500万円以上の案件が多数。職務経歴書を元にした三段階評価によって、選考通過の可能性がわかる。新規事業や外資系企業へのチャレンジも。
faviconeh-career.com
Buy Me A Coffeeのbutton

目次