theme | title | info | drawings | mdc | overviewSnapshots | fonts | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ohkime |
React CompilerとFine-Grained Reactivityと宣言的UIのこれから |
JSConfJP 2024
|
|
true |
true |
|
JSConf JP 2024 - TOMIKAWA Sotaro (ssssota)
冨川 宗太郎
株式会社ZOZO フロントエンドエンジニア(テックリード)
{
"x": "ssssotaro",
"bsky": "ssssota.bsky.social",
"github": "ssssota"
}
仕事ではReact、趣味はSvelteかPreactを使っていることが多いです。
<style> ruby { font-size: 3rem; } rt { font-size: 1rem; } img { position: absolute; right: 5rem; top: 5rem; width: 10rem; height: 10rem; } </style>宣言的UIはWeb開発の標準となり、Webだけでなくモバイルアプリケーションやデスクトップアプリケーションにも広がりを見せている。
- Web: React, Vue.js, Svelte, Preact, etc...
- モバイル: SwiftUI, Jetpack Compose, React Native, Flutter, etc...
APIはそれぞれ少しずつ異なるものの、
「状態をもとにUIを宣言する」 という基本的な考え方は共通している。
今日はWeb開発における宣言的UIのこれまでとこれからを考える。
10年前、“仮想DOMという概念が俺達の魂を震えさせ”ていた。
(仮想DOMは宣言的UIを実現するための手段で宣言的UIそのものではないが)
宣言的UIをここまで広めたのはReactやVue.jsのような仮想DOMを使ったライブラリの存在といっても過言ではない。
宣言的UIのこれからを語る上で、仮想DOMをまずは振り返る。
function App({ name }) {
return (
<div>
<h1>Hello {name}!</h1>
<p>Welcome to the session.</p>
</div>
);
}
例えばこんなコンポーネントを考える。
0. 初回レンダリング時の仮想DOM
1. 状態変化時 仮想DOMを再構築する
2. 仮想DOMが構築できたら、差分を検出する (reconciliation / diffing)
3. 検出した差分をもとに、実際のDOMに反映する (render, commit)
「仮想DOMは純粋なオーバーヘッドである」
Svelte作者のRich Harris氏が6年前に公開したブログのタイトル。
今後の宣言的UIを考える上での重要なキーワード。
- 仮想DOMの差分検出自体コストがかかる
- 仮想DOMツリーを探索して、効率よく実際のDOMに適用するための差分を検出する必要がある
- Reactでは
$O(n)$ のアルゴリズムを使っているとされる(コスト小)
- 仮想DOMの構築自体コストがかかる
- 仮想DOMツリーの構築では様々な計算やアロケーションが何度も発生する
- 各種配列、仮想DOM自体のオブジェクト、インライン関数、、、
- 仮想DOMツリーの構築では様々な計算やアロケーションが何度も発生する
今年春発表されたReact Compiler (React Forgetは2021年発表)。
これが、先の問題を解決する。
- 仮想DOMの構築自体コストがかかる
- 仮想DOMツリーの構築では様々な計算やアロケーションが何度も発生する
- 各種配列、仮想DOM自体のオブジェクト、インライン関数、、、
様々な計算やアロケーションとは?どのように解決するのか?
function App({ name }) {
return <h1>Hello {name}!</h1>;
}
function App(t0) {
const $ = _c(2);
const { name } = t0;
let t1;
if ($[0] !== name) {
t1 = <h1>Hello {name}!</h1>;
$[0] = name;
$[1] = t1;
} else {
t1 = $[1]; // nameが同じ→再利用
}
return t1;
}
function List({ items }) {
return (
<ul>
{items.map((item) => {
return <li>{item}</li>;
})}
</ul>
);
}
function List(t0) {
const $ = _c(4);
const { items } = t0;
let t1;
if ($[0] !== items) {
t1 = items.map(_temp);
// . . .
}
// ↓トップレベルへ移動
function _temp(item) {
return <li>{item}</li>;
}
useMemo
や React.memo
を使えばできなくもないが、
開発者がそれを意識しなければならなかった。
これらのコストをReact Compilerが最適化する。
- 仮想DOMオブジェクトをキャッシュする
- インライン関数をトップレベルに移動する/キャッシュする
状態変化時、通常は状態が変化したコンポーネントの子孫も再構築されるが、
React Compilerでは再構築されるコンポーネントを最小限に抑える。
0. 初回レンダリング時の仮想DOM
1. 状態変化時 仮想DOMを再構築する
2. 仮想DOMが構築できたら、差分を検出する (reconciliation / diffing)
3. 検出した差分をもとに、実際のDOMに反映する (render, commit)
ここまで、ReactのReactによるReactのためのReact Compilerを使った Virtual DOM is pure overhead への対応を見てきた。 これは仮想DOMと共存する道の1つと言える。
(仮想DOMのVue.js SFCもコンパイルを伴うため最適化が行われている)
一方で、仮想DOMを使わない宣言的UIも存在する。
いまでは様々なフレームワークが利用しているJSX。
当初はFacebook(現Meta)がReactのために開発した言語拡張。
JSXはReactとともに普及し、
現在ではマークアップのデファクトスタンダードとなっている。
独自文法とは異なり周辺ツールチェーンの恩恵を受けやすいのも大きなメリット。
- Parser
- Linter / Formatter
- Transformer
- etc...
3年前、SolidJS(v1)が登場。Reactと同じくJSXを採用しながら、仮想DOMを使わない宣言的UIを実現。
状態は全てSignalsで管理。Signalsの値が変化すると、それを検知して該当のSignalsが使われたDOMを更新。
これがいわゆるFine-Grained Reactivity。
Fine-grained: きめの細かい
Fine-Grained ReactivityのベースにあるのがSignals。SignalsはStreamやObservableのような概念で、単一の値を持ちその値が変化すると通知できる。
StreamやObservableではなく、Signalsが宣言的UIで重宝されるのはインターフェースと柔軟性のバランスが取れているため。
これらは状態を購読する例。
// React Hooks
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
}, [count]);
// Svelte (svelte/store)
const count = writable(0);
count.subscribe((value) => {
console.log(value);
});
// Vue.js (@vue/reactivity)
const count = ref(0);
watchEffect(() => {
console.log(count.value);
});
SolidJSのコンポーネントは1度しか実行されない。
1度実行でコンポーネントのすべての状態を返す必要がある(イメージ)。
また、それゆえの制約がある。
算出プロパティを使う時は関数にする
```jsx
function App() {
const [count, setCount] = createSignal(0);
const double = count() * 2;
return (
<>
<p>{count()} * 2 = {double}</p>
<button onClick={() => setCount((c) => c + 1)}> +1 </button>
</>
);
}
```
```jsx
function App() {
const [count, setCount] = createSignal(0);
const double = () => count() * 2;
return (
<>
<p>{count()} * 2 = {double()}</p>
<button onClick={() => setCount((c) => c + 1)}> +1 </button>
</>
);
}
```
早期リターンができない
```jsx
function App() {
const [count, setCount] = createSignal(0);
if (count() === 0) {
return <button onClick={() => setCount(1)}>Start!</button>;
}
return <p>Count is {count()}</p>;
}
```
```jsx
function App() {
const [count, setCount] = createSignal(0);
return (
<Show when={count() === 0} fallback={<p>Count is {count()}</p>}>
<button onClick={() => setCount(1)}>Start!</button>
</Show>
);
}
```
いずれもFine-Grained Reactivityを実現。(Svelte 5は10月リリース、Vue VaporはWIP)
SolidJSとは異なり独自の文法を提供しているため、「SolidJSのような制約を軽減している」とも言える。 そもそもリターンを書かないから早期リターンもない。
一方で、JSXではない故の問題もある。
最近はRust製の高速なツールチェーンが登場しているが、対応が後回しになりがち。
いずれも開発者が書いたコードがコンパイルされ、関数コンポーネントになる。
この関数コンポーネントは実際のDOMを返す(Svelteは若干異なるが省略)。
<!-- コンパイル前 -->
<script setup>
defineProps(["name"]);
</script>
<template>
<h1>
Hello {{ name }}!
</h1>
</template>
// コンパイル後 (一部手で調整)
const t0 = _template("<h1></h1>")
function render(_ctx, $props) {
const n0 = t0()
_renderEffect(() => {
_setText(n0, "Hello ", $props.name, "!")
})
return n0
}
Virtual DOM is pure overhead に対する1つの答えが
Fine-Grained Reactivity。
そもそも仮想DOMを使わなければ、仮想DOMのオーバーヘッドはなくなる。
オブジェクトや関数のアロケーションも、関数コンポーネント自体は一度しか呼ばれないので問題にならない。
主なプレイヤーはSolidJS、Svelte 5、Vue Vapor。
(Vue Vaporは仮想DOMモードとの併用も可能)
Fine-Grained Reactivityの基本はSignals。
このSignal、各フレームワークが独自に実装している。当然互換性はない。
そこで、TC39のプロポーザルとしてSignals標準化の動きがある。(Stage 1)
現状すぐに使えるわけではないが、標準化されれば各Fine-Grained Reactivity系フレームワークの互換性が向上する可能性もある。 (パフォーマンスはそれほど変わらない)
仮想DOMが宣言的UIを広めてきたが、仮想DOMを使わない宣言的UIも勢力を拡大している。
Reactは仮想DOM由来の問題を解決するためにReact Compilerを開発中。
React Compilerは名前の通りReact(のコンポーネント)をコンパイルする。
他のライブラリはSignalを使ったFine-Grained Reactivityがアツい。 Fine-Grained Reactivityでは仮想DOMを使わず、状態に追従する実際のDOM要素を作り出す。
いまを考えると、これからの宣言的UIには次のような要素が求められると考えられる。
- パフォーマンス
- 命令型のコード(=フレームワークなし)に漸近するスピード
- 開発体験
- 開発者が違和感のないコードを書けること
- 外部ツールとの親和性
- 互換性
- 既存コードとの親和性
宣言的UIはWeb開発の標準となった。
React CompilerやFine-Grained Reactivityは、
これからの宣言的UIのキモになるかもしれない。
一方で、仮想DOMが極端に遅いわけではない。
我々は現実的なスピードで動作するWebアプリを作っているし、使えている。
新しい技術を注視しつつ、いまある仮想DOMなどの技術と課題を見つめていけばよい。
- 宣言的UIはWebのものではなく、広く使われるようになっている
- 仮想DOMは宣言的UIを広めたが、オーバーヘッドも問題視されている
- React CompilerはReactの仮想DOMオーバーヘッドを解決する
- Fine-Grained Reactivityは仮想DOMを使わず、宣言的UIを実現する
- これからの宣言的UIには高いパフォーマンス・開発体験・互換性が求められる
- 仮想DOMも死なないので引き続き魂を震わせてOK