Signals と Signals.そして Retained UI.
序
ここ 2, 3 年で Signals という言葉は急激に広がった. しかし,この言葉はしばしば二つの異なるものを同時に指している.
ひとつは,Vue の ref() や Solid の createSignal() のような,ランタイムで値と依存関係を保持するリアクティブ・プリミティブ という意味での Signals だ.
もうひとつは,Svelte や Solid,そして Vue Vapor のように,どの DOM がどの依存にぶら下がるかという tracker まで含めて更新系を語る文脈 での Signals である.
私はこの二つを意図的に分けて考えた方がよいと思っている. 前者は「値のリアクティビティ」であり,後者は「レンダラのリアクティビティ」だ.
そしてこの区別を曖昧にすると,Signals = VDOM の終わり のような雑な結論に流れやすい.
しかし実際には,TC39 Signals proposal 自身が FAQ の中で,Signals は VDOM にも,native DOM にも,両者の組み合わせにも載せられると明言している.
つまり,Signals は 描画方式そのものではない.
この話をするためには少し歴史に戻る必要がある.
Knockout.js と Observable の時代
Signals の流行を見るたびに,私は Knockout.js を思い出す.
Knockout はかなり早い時期から observable と computed を持ち,依存関係を自動追跡していた.
しかも Knockout のドキュメント は,宣言的 binding 自体が computed observables として実装されている と説明している.
これは重要だ. 今日我々が「Signals 的」と呼んでいるもののかなりの部分は,実はかなり昔からある. 値を observable に包み,依存を追跡し,変更時に UI の一部を更新するという構図は,新しいアイデアではない.
ただし,その後しばらく標準化の文脈で前に出てきたのは Signal ではなく Observable だった. TC39 の Observable proposal は,DOM event や timer や socket のような push-based な複数値の stream を標準化しようとしていた. 現在は WICG Observable proposal 側で継続しており,そこでも history として 2015 年の TC39 提案,2017 年の WHATWG DOM issue,2019 年の再活性化が整理されている.
ここで一度整理しておきたい.
- Observable が主に扱うのは,時間方向に流れる event stream だ.
- Signals が主に扱うのは,「いまの値」とその依存グラフだ.
両者は近いが同じではない. Observable は temporal であり,Signal は current-value oriented だ. だから,TC39 における Observable の歴史と,現在の Signals proposal は連続してはいるが,そのまま同一視はできない.
しかも Signals proposal は,framework authors の協調の上で,developer-facing surface API よりも,underlying signal graph の core semantics を揃える ことに重心を置いている. ここでもう既に,「Signals とは end-user API ではなく,framework の下に潜る基盤である」という意識が見えている.
Signals と Signals.
ここからが本題だ.
私が言いたいのは,Signals という語には少なくとも二つのスコープがある,ということだ.
1. ランタイム・プリミティブとしての Signals
これは最も狭い意味での Signals だ.
Vue の reactivity docs は,Vue 3 では reactive() が Proxy を使い,ref() が getter / setter を使って track() / trigger() を実現していると説明している.
また同じページで Vue は,自身の reactivity system が primarily runtime-based であると明言している.
Solid の createSignal() も同様に,getter が reactive context 内で依存を追跡し,setter が dependent computations を通知する primitive として説明されている.
この層での Signals は,かなり素朴に次のようなものだ:
signal cell -> track reads -> trigger writes -> rerun effects
ここで主役なのは 値 である. どの値が読まれたか,どの effect がそれに依存しているか,という依存グラフがランタイムに構築される.
Vue の ref も,Solid の createSignal も,この文脈でまず理解できる.
2. DOM tracker まで含めた Signals
しかし,もうひとつ別の文脈がある.
Svelte は,compiler によって browser での仕事を最小化する framework であると自身を説明している. TC39 Signals proposal の FAQ も,Svelte 5 が runes を内部の signals library に transform する例として挙げている. Solid の README は,template を real DOM nodes に compile し,whole button を rerender するのではなく,必要な場所だけを update するコードを生成することを示している. また Solid Docs の fine-grained reactivity は,React が component 全体を再実行しがちなのに対して,Solid は target attribute を更新すると説明している.
このとき Signals は,もはや単なる値 container ではない. どの signal がどの DOM text node / attribute / binding を更新するか,という DOM 側の tracker まで含めた更新文脈になる.
signal graph -> compiler-known DOM sinks -> exact DOM mutation
ここでは renderer の仕事の一部が dependency graph に吸い込まれている. 再実行の単位は component 全体ではなく,もっと細い DOM sink に近づく.
この意味での Signals は,値の primitive というより 描画アーキテクチャの名前 である.
ここでは同じ Signals という語を重ねている.
Retained UI
では VDOM や React Fiber はどこに位置づくのか.
私はこれを Retained UI と呼びたい. つまり,一度 desired UI をメモリ内の node / frame / tree として持ち,それをもとに更新を管理するランタイムである.
React の Render and Commit は,render で component を呼び,commit で DOM に反映すると説明している. 再 render の際には changed props を計算し,commit では minimal necessary operations を適用する.
さらに React Fiber architecture では,
- render 時に app を記述する tree が memory に生成されること
- その tree を diff して DOM operations を計算すること
- Fiber は unit of work であり,virtual stack frame のようなものだということ
が説明されている.
これは非常に重要だ. React において主役なのは,signal dependency graph ではなく in-memory tree / fiber graph の方だ. state update はその tree を再計算するための引き金であり,最終的な DOM update の主体は reconciler 側にある.
つまり React では,
state update -> rerun components -> new in-memory tree -> reconcile -> commit DOM
である.
ここで Reactivity は存在しないわけではない. しかしそれは Vue や Solid のように first-class な dependency graph として前景化しているわけではない. React の場合,reactivity は主に どの component subtree を再評価するか という invalidation の問題として現れ,実際の更新は Retained UI が引き受ける.
Vue はその中間にいる
面白いのは,Vue が昔からこの二層を両方持っていることだ.
- template を render function に compile し
- mount を reactive effect として実行し
- dependency change 時に effect を rerun して新しい VDOM tree を作り
- patch / reconciliation で DOM を更新する
と説明している.
さらに Vue は,同じページで patch flags や tree flattening を用いた compiler-informed virtual DOM を説明している. つまり Vue は昔から,
- 下層には
ref/reactive/ effect というリアクティビティ - 上層には compiler hints 付き VDOM runtime
を重ねていた.
これは React と Solid の中間というより,Signals と Retained UI を両方持つ構造 と言った方がよい.
だから Vue を見ているとわかる. Signals は VDOM の否定ではない. Signals は VDOM の下にも置けるし,上にも別の renderer を置ける. 実際 TC39 Signals proposal の FAQ も,Signals は VDOM にも native DOM にも combination にも載ると言っている.
Vue Vapor の未来
この観点から Vue Vapor を見ると,話はかなり明確になる.
2025 年 12 月 23 日の Vue 3.6.0-beta.1 release note は,Vapor Mode を
- Vue SFC の new compilation mode
- baseline bundle size と performance 改善を目指すもの
- 100% opt-in
- feature-complete だが still unstable
と説明している.
しかも同じ release note には,かなり重要なことが書いてある.
- Vapor-only mode では Suspense を直接は持たないが,VDOM Suspense の中で Vapor component を render できる
createApp側にvaporInteropPluginを入れると,VDOM app の中で Vapor component を使える- 逆に Vapor app 側でも interop plugin を入れれば VDOM component を使える
- ただし mixed nesting には rough edges があり,distinct regions を推奨する
これはかなり示唆的だ. 少なくとも近未来の Vue Vapor は,「VDOM を完全に捨てて全面移行する新しい Vue」ではない. むしろ 高性能 subset としての Vapor を,既存 Vue runtime と interop させながら段階的に広げる構想に見える.
Vapor Roadmap でも,
- SSR / Hydration
- Template Ref Interop
- Suspense support
- Vue Router
- Pinia
- Nuxt.js
- DevTools Integration
- Vue Test Utils
といった項目が並んでいる.
ここから読み取れるのは単純だ. Vapor の本質的な課題は,単に「速い DOM 更新ができるか」ではない. Vue ecosystem 全体とどう噛み合うか が課題なのだ.
つまり Vue Vapor の未来は,私はこう見る.
- VDOM が legacy residue として残るのではない
- Vapor が perf-sensitive path を担う高性能 subset になる
- VDOM runtime は interop / ecosystem / library compatibility の母体として当面は重要なままである
- そして両者の境界が,より明示的に設計されていく
これはむしろ Vue らしい. Vue は昔から progressive / incrementally adoptable であることを重視してきた. Vapor が将来成功するとしても,その成功の仕方は「全部置き換える」より,「適切な境界を持って共存する」に近いはずだ.
なぜこれは React との比較で重要なのか
React では Reactivity よりも Retained UI の方がコアであり,Fiber はその scheduling / reconciliation / prioritization を担う. 一方で Vapor / Svelte / Solid Compiler の方向は,依存追跡と DOM sink tracking を近づけることで,保持された tree への依存を薄くしようとする.
ここで比較すべきなのは「どちらが上か」ではない. 比較すべきなのは,どのレイヤが更新責任を持つか だ.
- React / classic VDOM: in-memory node runtime が更新責任を持つ
- runtime signals: signal graph が invalidation を持つが,renderer は別にいる
- compiler + DOM tracker signals: signal graph が renderer の責務の一部まで持ち始める
この三つは別物だ.
そして Vue は今,その全部を一つの体系の中に保持したまま前進しようとしている.
ref は残り,VDOM mode も残り,その上で Vapor が増える.
私はこれをかなり健全だと思っている.
Signals は魔法の銀の弾丸ではない. また VDOM は単なる過去の遺物でもない. Retained UI は,component composition,dynamic rendering,ecosystem interop,debuggability,tooling との整合において依然として強い. 一方で compiler-guided な Signals. は,hot path の細粒度更新と baseline size の面で強い.
問題は常に「どちらを捨てるか」ではなく,「どこに境界を引くか」なのだ.
これは私の個人的な考察であり,Vue Team / React Team / TC39 の公式見解ではありません.