Qwik は VDOM を「控えめに」に使用する - ??
対象読者
- Qwik について何となく知っている
- Qwik がどのように DOM を操作しているのか気になる
???
要約
- Qwik は、 vDOM を用いることもありながら、同時に SolidJS のような直 DOM 置き換えを使用することがある
- 構造的な変化を伴うか伴わないかによってそれは判断される
- 控えめな仮想 DOM は優れた開発体験を提供する
- 例
export const Component = component$(() => { const count = useSignal(0) // 構造的でない return <div> <div>Count: {count.value}</div> </div> // 構造的 return <div> {count.value > 10 ? <h1>カウントは10以上です</h1> : <div>カウントは10未満です</div>} </div> })
これらは理解できます。
三項演算子を使いたいとき、Vue Vapor や Svelte は独自テンプレート言語なので容易に直 DOM コードにコンパイルできます。しかし、SolidJS は JSX なので三項演算子を直 DOM にコンパイルするのは難しいです。だから Solid.js は Show コンポーネントなどを実装しているのですが、TypeScript との相性が悪いです。
Qwik はこのように三項演算子を使えるようにしつつ高速にするために「控えめ」な vDOM を使用しているのでしょう。
試しに以下の Qwik のコードをコンパイルしてみましょう。
import { component$, useSignal } from '@builder.io/qwik'
export default component$(() => {
const count = useSignal(0)
return <button onClick$={() => count.value++}>
{count.value}
</button>
})
以下のようになります。
import { u as useSignal, _ as _jsxQ, q as qrlDEV } from './_qwikCore.js';
import './_qwikPreloader.js';
const app_component_Ncbm0Trxwgc = ()=>{
const count = useSignal(0);
return /*#__PURE__*/ _jsxQ("button", null, {
onClick$: /*#__PURE__*/ qrlDEV(()=>import('./app.tsx_app_component_button_onClick_9tyWdQvbkh0.js'), "app_component_button_onClick_9tyWdQvbkh0", {
file: "/app.tsx",
lo: 149,
hi: 168,
displayName: "app.tsx_app_component_button_onClick"
}, [
_fnSignal((p0)=>p0.value, [
count
])
])
}, count, 3, "4e_0", {
fileName: "app.tsx",
lineNumber: 5,
columnNumber: 10
});
};
export { app_component_Ncbm0Trxwgc };
めっちゃ React みたいな vDOM 使ってるじゃん!!!!!
参考までに React/SolidJS/Svelte のコンパイル結果を置いておきます。
React
import React from 'react';
import { jsxs as _jsxs } from "react/jsx-runtime";
export default function App() {
const [count, setCount] = React.useState(0);
return /*#__PURE__*/_jsxs("button", {
onClick: () => setCount(count + 1),
children: ["Count: ", count]
});
}
SolidJS
import { template as _$template } from "solid-js/web";
import { delegateEvents as _$delegateEvents } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
var _tmpl$ = /*#__PURE__*/_$template(`<button>Count: `);
import { createSignal } from 'solid-js';
import { render } from 'solid-js/web';
function App() {
const [count, setCount] = createSignal(0);
return (() => {
var _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
_el$.$$click = () => setCount(count() + 1);
_$insert(_el$, count, null);
return _el$;
})();
}
render(App, document.getElementById('app'));
_$delegateEvents(["click"]);
Svelte
import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';
import * as $ from 'svelte/internal/client';
var root = $.template(`<button> </button>`);
export default function App($$anchor) {
let count = $.mutable_source(0);
var button = root();
var text = $.child(button);
$.reset(button);
$.template_effect(() => $.set_text(text, `Count: ${$.get(count) ?? ''}`));
$.event('click', button, () => $.update(count));
$.append($$anchor, button);
}
紐解いていきます。
コンポーネント単位 vDOM
Qwik は、コンポーネント単位でレンダリングしています。[1]
React の State が変化したときのことを考えてみてください。ルートのコンポーネントが呼び出され、React Node (vDOM) が生成されます。それをもとに前回の vDOM との差分を計算して DOM に再レンダリングします。
それに対して Qwik は、コンポーネント単位で vDOM の差分計算を行っているのです。コンポーネントの中で Signal が変更された場合、そのコンポーネントのみが再実行され、差分が適用されます。
Signal による DOM への介入
前述した Qwik のコンパイル後のコードでは、カウンタの Signal count
を .value
で参照せずに渡しています。Signal のオブジェクトごと子要素にしているイメージです。
Signal から DOM へ介入しているみたいなコード[2]がありますが、おそらく内部的に count
の Signal に依存している DOM のノードを count
へ注入し、count
の変更時に effect として直接 DOM のノードを置き換えているのだと思います。
まとめ
多分こんな感じ。コンポーネント単位 vDOM と Signal から直接 DOM を操作するという 2 つを組み合わせることによってパフォーマンスを高めながら vDOM を控えめに使用している。
訳わからん。
Discussion