VueVaporModeの「仮想DOMを使わない」とはどういうことなのか

## はじめに
:::note
この記事は[リンクアンドモチベーションアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/lmi)の16日目の記事です。
:::
Vue.jsを使っている開発者の皆様、VueVaporModeをご存知でしょうか。
私自身は「Vueの新しい機能なのかな？」くらいの認識だったのですが、この機会に調査してみたので読んでいただけたら幸いです。

## VueVaporModeとは？
![cb6842aa-7d01-2c09-b5cf-f6d2642ad2cd.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/887131/dd596e1f-5747-cf91-13cc-968f0b4e4a0a.jpeg)
VueVaporModeは、Vue.jsが現在開発中の次世代のコンパイル戦略を採用した実装のことです。
このモードをONにすることで、これまで採用されていた「仮想DOM」を使用しせずにコンポーネントを実装することができます。

## 仮想DOMを使わないってどういうこと？
私自身、SPA（シングルページアプリケーション）は仮想DOMが前提であるとい捉えてしまっていたため、VueVaporModeの概要に触れたさいに「どういうこと・・・？？」と少し混乱しました。
ただ、最近名前を聞くことが増えているSvelteやSolid.jsでは仮想DOMは採用されていないそうです。
Vue.jsでもSolid.jsに触発されてVueVaporModeを開発したそうですが、これまでの仮想DOMを使った実装とどのような違いがあるのか整理してみました。

## 仮想DOMを利用した差分レンダリングの仕組み
Vue.jsではHTMLの実際のDOM（Document Object Model）とは別に、DOMの構造を表すオブジェクトを裏側で持っています。
このDOMの構造を表すオブジェクトを仮想DOMと呼びます。
Vue.jsのコンポーネントが描画している値に変更があった場合、まずはその変更を仮想DOMに反映します。
次に、仮想DOMと実際のDOMを比較し、検出した差分を実際のDOMに反映しています。
簡単ですが、これが仮想DOMを利用した差分レンダリングの仕組みです。
![SCR-20241216-cbad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/887131/ffe0fceb-d9df-7ac9-a2cc-2b2f67efd080.png)

## 仮想DOMの課題
仮想DOMには多くの利点もありますが、課題もありました。
それはパフォーマンス面での懸念です。
仮想DOMは依存する値が更新される度に新たな仮想DOMが生成されるため、まず生成自体に少なくないコストがかかります。
さらにそこから現時点の仮想DOMとの比較も必要なため、反映にもコストがかかっています。
加えて仮想DOMのオブジェクト自体が大きなオブジェクトになることが多いため、メモリの消費も少なくもありません。

## 仮想DOMを使わない場合はどうするの？
仮想DOMの仕組みと課題が分かったとことで、本題のVueVaporModeの実装を見ていきましょう。
冒頭で触れたように、VueVaporModeでは仮想DOMを使いません。
その場合、依存する値が更新された場合にどのようにDOMに変更を反映しているのでしょうか。
VueVaporModeの[Playground](https://vapor-repl.netlify.app/#__VAPOR__eNp9UsFu2zAM/RVOOzgBEhvDdgrcbF3RYt2hKbqhu+ji2kyiTpYESfY8GP73UfKceW1RA4YsPj7y8dE9OzcmbRtkG5a70grjwaFvDMhCHc44846zLVeiNtp66MHifgUH9BeNtaj8tXK+UCXCAHura0ioVMIVV6UmBGp3gLPAWSRfUEoNP7SV1ZtkyVWWwSfv1tgZLP0ardV2Ygl3X1A7Yj5vtFimbQC5yrNRL6mji8fayMIj3QDy47tt38fuw5BndItREw+A+/Pb3R3kDyFn6vURkt1NAhs6rq6SSHsYWdlIy4UyjYd2XesKJTlD1TmDjMA8m3VnKyZUhV169LWcuep/GwwsXTUSo6dBSnhO3pYWqUbUQ1uZW5rFmcnYJ5yQNial2d9FzpL+r7egd5nWulF+kbwtjEmWJwvzSrQgKtJHcRKXZxTY0ije0Ur24pA+Oq1omj5U56zUtRES7c54QSvjbAMRCVhBe/71Nca8bXA1xcsjlj9fiD+6LsQ4u7Xo0LZkzgnzhaVfYIQvv91gR98ncLLyFfAOnZZN0DimfW5URbJneVHtdXRTqMN3d9l5VG4aKggNmUPM54z8vXhl9H9y36cfIo+rgQ1/AI1HHRM=)を使ってコードを見ていきましょう。

## VueVaporModeがONのコンパイルされたコード
```js
import { children as _children, vModelText as _vModelText, withDirectives as _withDirectives, delegate as _delegate, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue/vapor';

const t0 = _template("<h1></h1>");
const t1 = _template("<input>");

function render(_ctx, $props, $emit, $attrs, $slots) {
  const n0 = t0(); // h1要素のテンプレートを生成
  const n1 = t1(); // input要素のテンプレートを生成

  _withDirectives(n1, [[_vModelText, () => _ctx.msg]]);
  _delegate(n1, "update:modelValue", () => $event => (_ctx.msg = $event));

  let _msg;
  _renderEffect(() => {
    _msg !== _ctx.msg && _setText(n0, (_msg = _ctx.msg));
  });

  return [n0, n1];
}
```
こちらがVueVaporModeをONにしてコンパイルしたJavaScriptのコード（一部抜粋）です。
```js
const t0 = _template("<h1></h1>");
```
まずはこの_template関数でコンポーネント内で使われているHTML要素を生成しています。
現状のVue.jsでは仮想DOMに対して要素を追加するところ、VueVaporModeでは内部的にdocument.createElementしており、実際のHTML要素を生成しています。
```js
_delegate(n1, "update:modelValue", () => $event => (_ctx.msg = $event));
```
次に_delefateで要素に対してイベントハンドラを登録しています。
この例ではinput要素でユーザーの入力があった場合に、コンポーネントインスタンスの変数に変更を反映しています。
```js
_renderEffect(() => {
  _msg !== _ctx.msg && _setText(n0, (_msg = _ctx.msg));
});
```
最後にrenderEffectです。
renderEffectは引数に渡したコールバック内で使われているリアクティブな値に変更があった場合に、コールバックが実行されます（watchのイメージ）。
このsetTextでmsgに依存している要素に最新の値を反映していますが、ここでも仮想DOMではなく実際のDOMに反映をしています。
setText内部では対象のDOM要素にたいして直接変更を反映しています。

以上のように、変更のあった部分を直接実際のDOMに変更を反映しているため、巨大な仮想DOMのオブジェクトを持つ必要もなく、差分を比較する処理も必要ありません。
これは大きなパフォーマンス向上が期待できますね！

## まとめ
まだ開発中の機能ですが、調べていく中で大きな可能性を感じる変更でした。
ちなみにVueVaporModeはアプリケーション全体に対してだけではなく、コンポーネント単位で使うこともできるそうです。
段階的な移行ができそうでとても助かりますね！
まだまだ不明点も多いので、今後も注目して調査していきます。