TL;DR
- v3は独自テンプレートのコンパイル時に、差分検知の最適化を行っている
- 「静的要素の巻き上げ」と「パッチフラグ」による最適化
- この最適化によって、レンダリングと差分検知処理の速度がv2より100%向上している
- JSXの場合は最適化されないため、独自テンプレートが推奨される
レンダリング関数とは
Vue.jsは、記法がJSXか独自テンプレートかに関わらず、最終的にVue.jsのレンダリング関数に変換されます。これはReactなど他の仮想DOMフレームワークも同じです。
例えばこのようなJSXテンプレートは、以下のように変換されます。
<div>
<span class="hello">Hello</span>
<span class="world">{userName}</span>
</div>
Vue.h("div", null, [
Vue.h("span", { class: "hello" }, "Hello"),
Vue.h("span", { class: "world" }, userName),
]);
Vue.h
はVue.jsのレンダリング関数です。第1引数にNodeの種類(タグ名やコンポーネント名など)、第2引数にprops(HTMLに渡すclass属性やコンポーネントに渡すprops)、第3引数に子要素を渡します。
Vue.js v3におけるレンダリング関数の最適化
v3は、独自テンプレートのコンパイル時にいくつかの最適化を行います。
早速出力コードを見てみましょう。
<template>
<div>
<span class="hello">Hello</span>
<span class="name">{{ userName }}</span>
</div>
<template>
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = _createVNode("span", { class: "hello" }, "Hello", -1 /* HOISTED */)
const _hoisted_2 = { class: "name" }
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_createVNode("span", _hoisted_2, _toDisplayString(_ctx.userName), 1 /* TEXT */)
]))
}
Vue.js v3は、以下のような最適化を行っています。
- 静的要素の巻き上げ
- createVNode(レンダリング関数)の第4引数に渡すパッチフラグ
- openBlock / createBlock による子要素の順番保証(今回は説明省略)
ちなみに出力されたrender関数は、コンポーネント内のリアクティブな値(data
やcomputed
など)が変更される度に実行されます。そしてその戻り値と以前の戻り値を比較し、差異があれば実際のDOMにパッチを当てます。
これは、仮想DOMフレームワークにおける基本的な差分検知処理です。
静的要素の巻き上げ
変化することのない静的な要素は、render関数の外で定数として保持しておきます。
// <span class="hello">Hello</span> の巻き上げ
const _hoisted_1 = _createVNode("span", { class: "hello" }, "Hello", -1 /* HOISTED */)
export function render(_ctx, _cache) {
// call `_hoisted_1`
}
静的要素を保持しておくとこで、何度render関数を実行してもオブジェクトの生成を繰り返す必要はありません。
さらに第4引数に渡している-1 /* HOISTED */
ですが、これは次のパッチフラグにて説明します。
レンダリング関数に渡すパッチフラグ
以下のように、第4引数に数字が渡されていますが、これも差分検知の最適化に用いられるものです。
_createVNode("span", { class: "hello" }, "Hello", -1 /* HOISTED */)
_createVNode("span", _hoisted_2, _toDisplayString(_ctx.userName), 1 /* TEXT */)
Vue.jsでは、パッチフラグが以下のように用意されています。
export const enum PatchFlags {
TEXT = 1,
CLASS = 1 << 1,
STYLE = 1 << 2,
PROPS = 1 << 3,
FULL_PROPS = 1 << 4,
HYDRATE_EVENTS = 1 << 5,
STABLE_FRAGMENT = 1 << 6,
KEYED_FRAGMENT = 1 << 7,
UNKEYED_FRAGMENT = 1 << 8,
NEED_PATCH = 1 << 9,
DYNAMIC_SLOTS = 1 << 10,
HOISTED = -1,
BAIL = -2
}
これは差分検知を行っているときに、ビット演算を用いて必要なものだけを比較するために用います。
例えば-1 /* HOISTED */
や1 /* TEXT */
の場合は、以下のように差分検知を高速化します。
if (flag < 0) {
return // -1(HOISTED)の場合はNodeが変更される可能性がないためスキップする
}
if(flag & PatchFlags.TEXT) {
// この要素のテキストは変更される可能性があるため、テキストの比較を行う
}
コンパイル時にパッチフラグを立てることで、このように効率よく差分検知を行えます。
Evan you氏のスライドによれば、これらの最適化によって差分検知処理は100%の速度向上をしています。
まとめ
以上がVue.js v3における最適化の内容です。
JSX/TSXを使用する場合はこの最適化が行われないため、独自テンプレートを用いることが推奨されます。レンダリング関数を最適化するBabelプラグインは理論上は実装可能だと思いますが、v3リリース時に用意されるかは定かではありません。
そのため、しばらくは独自テンプレート+VSCodeのVeturで型推論を効かせる開発スタイルがスタンダードになるでしょう。