仮想DOMの理解とSvelte/SolidJSがそれを採用しない理由
Wenhao Wang
Dev Intern · Leapcell

はじめに
フロントエンド開発の状況は、新世代のJavaScriptフレームワークによって劇的に再形成されてきました。長年、ReactやVueは、仮想DOM(Virtual DOM)の革新的な利用により、効率性と開発者フレンドリーな抽象化を約束しながら、議論を支配してきました。仮想DOMは、UI更新を管理するためのベストプラクティスとして広く受け入れられ、ほぼ普遍的な最適化手法となりました。しかし、フロントエンドエコシステムが進化し続けるにつれて、SvelteやSolidJSのような新しいプレーヤーが、この確立されたパラダイムに挑戦しています。彼らは、仮想DOMは巧妙であるものの、それ自体に複雑さとオーバーヘッドをもたらすと主張し、さらなるパフォーマンスとシンプルさを目指す代替戦略を提案しています。この議論は単なる学術的なものではありません。今日の、そして未来のWebアプリケーションの構築と最適化方法に重大な影響を与えます。仮想DOMが実際に何であるか、そしてこれらの新しいフレームワークがそれなしでより良いものを達成できると信じる理由について掘り下げてみましょう。
コアコンセプト
代替案を検討する前に、関連する基盤技術を理解することが重要です。
DOM (Document Object Model)
DOMは、Webドキュメントのためのプログラミングインターフェースです。ページ構造をツリー構造で表現し、各ノードはドキュメントの一部(例:HTML要素、属性、テキストノード)を表すオブジェクトです。DOMへのインタラクション(要素の追加、削除、更新など)は、ユーザーの画面に変更を直接反映します。DOMの直接操作は、特に多くの変更が急速に発生する場合、ブラウザによるレイアウト再計算や再描画を頻繁にトリガーするため、遅くなる可能性があります。
仮想DOM (Virtual DOM)
仮想DOMは、実際のDOMの軽量なインメモリ表現です。アプリケーションの状態が変更されると、新しい仮想DOMツリーが作成されます。この新しいツリーは、「diffing」と呼ばれるプロセスで、以前のツリーと比較されます。diffingアルゴリズムは、実際のDOMを更新するために必要な最小限の変更セットを特定します。これらの変更はバッチ処理され、1つの最適化された操作で実際のDOMに適用されます。これにより、直接的なDOM操作が最小限に抑えられ、パフォーマンスの向上が目指されます。
カウンターを表示するシンプルなコンポーネントを考えてみましょう。
// React風の疑似コード function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
setCountが呼び出されると、Reactは以下を実行します。
Counterコンポーネントを再レンダリングし、新しい仮想DOMツリーを生成します。- この新しいツリーを以前のツリーと比較(diff)します。
<p>タグ内のテキストコンテンツのみが変更されたことを特定します。- 実際のDOMのその特定のテキストノードのみを更新します。
これにより、ブラウザは<div>や<button>要素全体を再レンダリングする必要がなくなり、計算量が節約されます。
仮想DOMが不要かもしれない理由
仮想DOMは、直接的で最適化されていないDOM操作よりも大幅な改善を提供しますが、SvelteやSolidJSのようなフレームワークは、それが独自のコストを持つ抽象化であり、より直接的でコンパイラー駆動型またはきめ細かなアプローチがより優れた結果を達成できると主張しています。
Svelte: コンパイラーアプローチ
Svelteは根本的に異なるアプローチを取ります。これはコンパイラーであり、コンポーネントコードを実行時に解釈するのではなく、ビルドステップ中に高度に最適化された命令型JavaScriptに変換します。これは、実行時に仮想DOMが存在しないことを意味します。Svelteコンポーネントは、状態が変更されたときにDOMを直接操作します。
同じカウンターのSvelteの例を見てみましょう。
<!-- Counter.svelte --> <script> let count = 0; function increment() { count += 1; } </script> <div> <p>Count: {count}</p> <button on:click={increment}>Increment</button> </div>
Svelteがこのコンポーネントをコンパイルすると、本質的に以下のようなJavaScriptコードが生成されます(概念的)。
// 生成されたSvelte出力(概念) function create_fragment(ctx) { let p, t0, t1, t2, button; return { c() { // 要素を作成 p = element('p'); t0 = text('Count: '); t1 = text(/*count*/ ctx[0]); t2 = space(); button = element('button'); button.textContent = 'Increment'; listener(button, 'click', /*increment*/ ctx[1]); ], m(target, anchor) { // 要素をマウント insert(target, p, anchor); append(p, t0); append(p, t1); insert(target, t2, anchor); insert(target, button, anchor); ], p(ctx, [dirty]) { // 要素を更新 if (dirty & /*count*/ 1) { // countが変更された場合 set_data(t1, /*count*/ ctx[0]); // テキストノードを直接更新 } ], d(detaching) { // 要素を破棄 if (detaching) { detach(p); detach(t2); detach(button); } del_listener(button, 'click', /*increment*/ ctx[1]); } }; }
countが変更されると、Svelteの生成されたコードは、新しいツリーを生成してdiffし、パッチを適用する必要なしに、特定のテキストノード(t1)を直接更新します。コンパイル時に「diffing」が行われます。なぜなら、SvelteはどのDOMの部分がどの状態変数に依存しているかを正確に知っているからです。これにより、仮想DOMのランタイムオーバーヘッドが排除されます。
SolidJS: 仮想DOMなしのきめ細やかなリアクティビティ
SolidJSは、Knockout.jsやMobXのようなリアクティブプログラミングパラダイムからインスピレーションを得ていますが、それをJSXに適用しています。JSXテンプレートを実際のDOMノードにコンパイルし、状態変数を「シグナル」でラップします。シグナルが変更されると、SolidJSは、そのシグナルに依存するDOM(または他の計算)のどの部分に正確に依存しているかを知り、それらの部分のみを更新します。これは、リアクティブプリミティブからDOM要素への直接的で一方向のデータバインディングを作成することで、仮想DOMを完全に回避します。
SolidJSでのカウンターを再度考えてみましょう。
// SolidJS import { createSignal } from 'solid-js'; function Counter() { const [count, setCount] = createSignal(0); return ( <div> <p>Count: {count()}</p> {/* count()はシグナルの値を読み取ります */} <button onClick={() => setCount(count() + 1)}>Increment</button> </div> ); }
setCount(count() + 1)が呼び出されると、countシグナルの値が更新されます。SolidJSは、<p>タグのテキストコンテンツ内のcount()呼び出しがこのシグナルに依存していることを知っています。次に、DOM内のそのテキストノードのみを直接更新します。Svelteと同様に、中間的な仮想DOMツリーはありません。JSXのSolidJSのコンパイルステップは、シグナル変更によって直接トリガーされる一連のDOM操作を実質的に作成します。この「きめ細やかなリアクティビティ」は、絶対に必要な更新のみが発生することを意味し、非常に効率的なDOM操作につながります。
重要な違いは、React/Vueが仮想DOMを使用して実際のDOM更新を最小限に抑えるのに対し、SvelteとSolidJSは仮想DOMを完全に排除することでさらに一歩進んでいるという点です。Svelteはコンパイル時の最適化を通じてこれを実現し、DOMを直接操作するJavaScriptを生成します。SolidJSは、状態変更を特定のDOMノードに直接マッピングするリアクティブグラフを通じてこれを実現し、これも直接DOM操作にコンパイルされます。
結論
仮想DOMは、非効率的なDOM操作の課題に対する優れたソリューションであり、フロントエンド開発に革命をもたらしました。しかし、フレームワークが進化するにつれて、説得力のある代替案が登場しています。SvelteとSolidJSは、実行時からコンパイル時(Svelte)に作業をシフトするか、非常に最適化されたきめ細かなリアクティブ接続(SolidJS)を確立することによって、仮想DOMとその再コンシリエーションプロセスのオーバーヘッドを完全に回避できることを実証しています。これにより、バンドルサイズが小さくなり、実行時パフォーマンスが向上し、多くの場合、開発者にとってよりシンプルなメンタルモデルが得られます。これらのフレームワークは、仮想DOMが私たちに役立ったことは確かですが、高速で効率的なWebアプリケーションを構築するための唯一の道、あるいは必ずしも究極の道ではないことを示しています。フロントエンドフレームワークの未来は、ブラウザのネイティブ機能との、より直接的で最適化された方法での対話に、ますますかかっているかもしれません。