【React, Vue.js, Svelte】10分で説明する仮想DOMとリアクティビティ

本稿は、筆者がよく使うフロントエンドフレームワークを対象として、仮想DOMとは何か、リアクティビティとは何かを説明するエントリーです。

「フロントエンドフレームワークなんて、どれもおなじでしょう??」という声も多いかもしれませんが、比べてみると意外と違うんだなーと感じてもらえたらと思います。

仮想DOMとは

レンダリング

コンピュータの世界において、「レンダリング(rendering)」とは、データ構造などの抽象的な情報から画像などの具体的な情報へ変換することを指します。

レンダリング(rendering)は、データ記述言語データ構造で記述された抽象的で高次の情報から、コンピュータプログラムを用いて画像・映像(動画)・音声などを生成することをいう。
引用:ja.wikipedia.org

フロントエンドフレームワークにおいても、DOMの表示や更新を指して「レンダリング」という言い方をよくするのですが、特にReactにおいては以下の一連の流れを指して、レンダリングと呼んでいるようです。

  1. レンダリングのトリガー
  2. コンポーネントの情報の更新
  3. DOMへの同期

(参考:react.dev)

フレームワークによって多少の解釈の差異はあるかと思いますが、本稿においてのレンダリングは上記の定義に則ろうと思います。

仮想DOM

一見すると難しそうな名前の「仮想DOM」ですが、内部的には実際のDOM構造を模したJSオブジェクトとして構成されています。

抽象的なデータ構造としての仮想DOMを、更新前後で比較して、その差分のみを効率的に検知・DOMに反映しようとするレンダリングの実装パターンを指して、「仮想DOM」と呼びます。

前回のレンダリング時と更新後の仮想DOMを比較して、更新がある部分だけをレンダリングすることにより、レンダリングコストを下げつつUIとデータの整合性をとることができるというメリットがあります。

リアクティビティとは

リアクティビティの定義

リアクティブ(Reactive)という単語自体は「反応的な」という意味を持ちますが、Web開発においてはどういう意味を持つのでしょうか。

この点に関して、Vue.jsの公式にはこんな記述があります。

リアクティビティーとは、宣言的な方法で変化に対応できるようにするプログラミングパラダイムです。

(引用:vuejs.org)

ポイントとしては「宣言的な方法」「変化に対応」という2点だと個人的には感じます。
Vue.jsの上記リンクでは、Excelをリアクティブなアプリケーションの例としてあげています。

これはすなわち、「Webアプリケーションにおいて想定される操作に対するUIを定義し、状態の変化に追従することができる」特性のことだと捉えられそうです。
上記の内容を、本稿における「リアクティビティ」と定義したいと思います。

宣言的なコード

フロントエンドフレームワークを扱う中で「宣言的」という言葉は目にする機会が多いと思います。

宣言的なコードと対比的に用いられる表現として「手続的なコード」という表現があります。
従来のJSやjQueryを用いたフロントエンド実装というのはまさに手続的なものでした。

  • Card01
  • Card02
  • Card03

たとえば、こちらは上のようなリストの1要素をクリックした際に、全てのリストを削除する処理をJSで記述した手続的なコードの例です。

処理の内容としては、以下のような流れになっています。
 ①リストの各要素にクリックイベントを付与
 ②リストの要素をクリックした際にその親ノードを取得
 ③親から見た子ノードが無くなるまでノードの削除処理を実行
 ④データとUIの整合性のため、cardList変数を空配列にする

こうした手続的コードの場合、以下のような問題点があります。

  • DOMの遷移がコード化・定義されていないため、ある1状態の再現が難しい
  • DOM操作をするためのマーカー用の属性を付与する必要がありノイズが多くなる場合がある
  • DOMの更新とデータの更新は別々で実装者の責任となり、保守性・可読性が悪い

一方、以下は同様のコードをReactで宣言的に記述したコードの例です。

ノードに対する直接的な処理を記述する必要はなく、実装者が記述する必要があるのは、データに対する変更処理のみです。

人によって見え方は様々でしょうが、おそらくこちらの方がよりすっきりと直感的に見えるのではないでしょうか。

上記のコード例のように、宣言的なコードというのはデータの管理を基点としてUIを構築するという考え方になっているため、アプリケーションの状態とUIを整合させることが容易になります。

リアクティビティのメリット

前述のように、JSでの手続的なDOM操作でアプリケーションの状態とUIを整合させるのはコストが大きく、保守性が悪いという問題があります。

そこで、各フロントエンドフレームワークでは「データバインディング」という方法を持って、データとUIの動的な更新を同期させる手法をとっています。


こうすることで、アプリケーション開発における以下のような開発上のメリットを達成しています。

  • 煩雑なUI制御のコストを小さくして重要なロジックに集中できる
  • プログラマの責任でデータとUIの整合性を担保しなくていい
  • データの状態に対応するUIテンプレートを定義する宣言的なコードで保守性が上がる

各フレームワークでの捉え方

React・Vue.js

React、Vue.jsではいずれも仮想DOMを採用しています。

先述のように、仮想DOM自体は実装パターンのことを指すので、各フレームワークごとに実装詳細は多少異なります。

Reactでは、初期はStackという純粋な木構造として実装されており、React16以降ではFiberという実装によって仮想DOMを実装しています。
(ちなみに、ReactのFiberはブラウザのみをプラットフォームとするものではなく、ReactNativeでも使われているので、厳密には仮想DOMとも違うかもしれません。Reactでは、Fiberのことを指して「リコンサイラー(reconciler)」と呼んでいたりします。)

また、Vue.jsにおいては、Vue2までは「Object.defineProperty」、Vue3からは「Proxy」というJS組み込みの機能を利用しています。

いずれも、データの値変更に対してインターセプトして何らかの処理を実行することができるという機能を持つものです。

Reactと異なる点として、ReactではFiber自体に状態管理機能が内包されている(*1)のに対し、Vue.jsでは先述のProxyを使って値の変化を検知し、そこに仮想DOMのレンダリングをフックさせる(*2)という方法をとっています。

Svelte

Svelteは、React、Vue.jsとは異なり、仮想DOMを使わないというアプローチをとっています。

React・Vue.jsいずれも仮想DOMを使うことで、「ランタイム」でDOMとデータの整合性を担保しています。一方、Svelteは「コンパイルタイム」で静的に整合性の担保を行います。

蛇足:脱仮想DOMへの動き?

仮想DOM自体はフレームワークごとにさまざまな工夫がされていて、本来は高いコストとなる差分検出処理を効率化する取り組みがなされています。

とはいえ、実際のDOMレンダリングに追加で処理が行われているということもあり、必ずしも期待しているパフォーマンスを出せるわけでもないようです。

Svelteではこのような仮想DOMのことを指して「純粋なオーバーヘッド(Pure Overhead)」と表現していたりもします。
(参考:svelte.dev)

今でこそ仮想DOMを採用したフレームワークは多く存在するのですが、そうした潮流ももしかしたら変わるのかもしれません。

実際に、Vue.jsではVaperModeという実験的機能の実装も進んでおり、これはまさに立つ仮想DOMに向けたもののようです。
(参考:icarusgk.hashnode.dev)

まとめ

  • 仮想DOMはデータとUIを効率的に整合し、レンダリングするための工夫
  • 仮想DOMと値の変更検知のロジックをもって、リアクティビティが実現されている
  • 最近は脱仮想DOMの動きも出てきているように見える

たまにjQueryのコードを見たりすると、いかに仮想DOMを使った各フレームワークの宣言的記法に助けられているかを実感します。

しかしながら、最近はSvelteやVue.jsのVaperModeのように脱仮想DOMの動きも出ているように見えるので、今後の流れに注目していきたいところです。


参考:
*1: Reactソースコード

*2: Vue.jsソースコード

前へ

Go言語のベンチマークについて

次へ

HTMLとCSSだけでロード中のぐるぐるを自作する