Signals と Signals.そして Retained UI.

2026年3月11日

ここ 2, 3 年で Signals という言葉は急激に広がった. しかし,この言葉はしばしば二つの異なるものを同時に指している.

ひとつは,Vue の ref()Solid の createSignal() のような,ランタイムで値と依存関係を保持するリアクティブ・プリミティブ という意味での Signals だ. もうひとつは,SvelteSolid,そして Vue Vapor のように,どの DOM がどの依存にぶら下がるかという tracker まで含めて更新系を語る文脈 での Signals である.

私はこの二つを意図的に分けて考えた方がよいと思っている. 前者は「値のリアクティビティ」であり,後者は「レンダラのリアクティビティ」だ.

そしてこの区別を曖昧にすると,Signals = VDOM の終わり のような雑な結論に流れやすい. しかし実際には,TC39 Signals proposal 自身が FAQ の中で,Signals は VDOM にも,native DOM にも,両者の組み合わせにも載せられると明言している. つまり,Signals は 描画方式そのものではない.

この話をするためには少し歴史に戻る必要がある.

Knockout.js と Observable の時代

Signals の流行を見るたびに,私は Knockout.js を思い出す. Knockout はかなり早い時期から observablecomputed を持ち,依存関係を自動追跡していた. しかも 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 が昔からこの二層を両方持っていることだ.

Vue の Rendering Mechanism は,

  1. template を render function に compile し
  2. mount を reactive effect として実行し
  3. dependency change 時に effect を rerun して新しい VDOM tree を作り
  4. 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 には,かなり重要なことが書いてある.

  1. Vapor-only mode では Suspense を直接は持たないが,VDOM Suspense の中で Vapor component を render できる
  2. createApp 側に vaporInteropPlugin を入れると,VDOM app の中で Vapor component を使える
  3. 逆に Vapor app 側でも interop plugin を入れれば VDOM component を使える
  4. ただし 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 の公式見解ではありません.

関連コンテンツ

記事一覧に戻る