前回、TODOアプリをVanilla JSとReactの両方で作り比べて、こんな結論になりました。
Vanilla JS:「どうやって画面を変えるか」を自分で全部書く
React :「何が変わったか」だけ伝えれば、あとはReactがやってくれる
この「何が変わったか」を見つけ出す仕組みこそが、今回の主役 Virtual DOM です。
Reactを勉強していると、だいたいこう説明されます。
「Virtual DOMだから速いんだよ」
……これ、実は半分正解で半分ウソです。
今日はその「半分ウソ」の部分まで含めて、Virtual DOMの正体を追いかけてみます。
- Virtual DOMって結局何なのか
- なぜ「直接DOM操作は重い」と言われるのか
- Reactはどうやって「何が変わったか」を見つけているのか(Reconciliation)
-
keypropで事故る初心者あるある - Virtual DOMの限界と、その先にある発想
盛りだくさんですが、順番に行きます。
1. Virtual DOMの正体:ただのJSオブジェクト
いきなり核心から言うと、Virtual DOMは特別な魔法ではなく、ただのJavaScriptオブジェクトです。
// これは実際のDOM要素
const realElement = document.createElement("li");
realElement.textContent = "牛乳を買う";
// → プロパティやメソッドが軽く100個を超える巨大なオブジェクト
// (style, addEventListener, appendChild, scrollIntoView... 数えきれない)
// 一方、Virtual DOMはこう
const virtualElement = {
type: "li",
props: {
children: "牛乳を買う"
}
};
// → ただのプレーンなオブジェクト。プロパティは type と props だけ
JSXでこう書いたとき、
<li>牛乳を買う</li>
これは裏側で React.createElement("li", null, "牛乳を買う") という関数呼び出しに変換されていて、上のようなプレーンオブジェクトを返しています。JSXは見た目がHTMLなだけで、正体は「オブジェクトを作る関数呼び出し」です。
実DOM要素は、ブラウザが管理する巨大で重量級のオブジェクトです。一方Virtual DOMは、ただの軽いJSオブジェクト。オブジェクトを作ったり比較したりするコストは、実DOMを直接触るコストよりずっと軽い。 これがVirtual DOMの出発点になります。
2. じゃあ、なぜ直接DOM操作は「重い」のか
ここで一度、「DOM操作は重い」の中身を分解してみます。
ブラウザは、DOMが変更されるたびに、だいたいこんな処理を行っています。
- Style計算 … どの要素にどのCSSが適用されるか計算する
- Layout(Reflow) … 各要素の位置とサイズを計算する
- Paint … 実際にピクセルを描く
- Composite … レイヤーを合成して画面に表示する
問題は、Layout(Reflow)が結構コストの高い処理だということです。1つの要素のサイズが変わると影響が周りの要素にも波及するので、ブラウザは「再計算が必要な範囲」を洗い出さなきゃいけません。
さらに厄介なのが、こういうコードです。
for (let i = 0; i < 100; i++) {
const li = document.createElement("li");
li.textContent = `TODO ${i}`;
list.appendChild(li); // ← ここでLayout対象になりうる
console.log(list.offsetHeight); // ← ここで強制的にLayoutを実行させてしまう
}
appendChild で要素を追加した直後に offsetHeight のようなレイアウト情報を読み取ると、ブラウザは「最新のレイアウトを教えるために、溜まってる変更を今すぐ全部計算して」と強制されます。これを**レイアウトスラッシング(Layout Thrashing)**と呼びます。上のコードは100回ループしているので、最悪100回Layoutが走る可能性があるということです。
これが「直接DOM操作は重い」の正体です。1回1回は大した処理じゃなくても、回数が増えると掛け算で効いてくる。
3. Virtual DOMの解決策:まとめて比較して、まとめて反映する
ここでVirtual DOMの出番です。基本方針はシンプルです。
1個変更するたびにDOMを触る
↓ じゃなくて
まずJSオブジェクトの世界で全部の変更を集める
↓
最後に「差分」だけをまとめて実DOMに反映する
この「差分を見つける」処理を 差分検出(Diffing)、更新前と更新後のツリーを比較して実DOMへの反映内容を決める一連の流れ全体を Reconciliation(調停) と呼びます。
そもそも、この「木」はどうやって作られているのか
比較の話に入る前に、大前提を確認しておきます。「木構造で比較する」と言っていますが、その木は実際どうやって組み立てられているのでしょうか。
1節で見た通り、1個のReact要素はただのオブジェクトでした。
{ type: "li", props: { children: "牛乳を買う" } }
これが複数ネストすると、こうなります。次のJSXを書いたとします。
<ul>
<li>牛乳を買う</li>
<li>卵を買う</li>
</ul>
これはReact.createElementの入れ子呼び出しに変換され、結果的にこういうオブジェクトができます。
{
type: "ul",
props: {
children: [
{ type: "li", props: { children: "牛乳を買う" } },
{ type: "li", props: { children: "卵を買う" } }
]
}
}
「木」の正体は、props.childrenが入れ子になっているだけのオブジェクトでした。JSXの見た目の入れ子構造が、そのままオブジェクトのデータ構造としての入れ子になる。だからこそ「木構造」として比較できるわけです。
ただしこれは、毎回の描画のたびに新しく作られる使い捨てのオブジェクトです。Reactが実際に保持し続けている木はこれとは別物で、各ノードがchild(最初の子)・sibling(次の兄弟)・return(親)という3本のポインタだけを持つ、Fiberという内部構造で表現されています。「子の配列」を持たせるのではなく「最初の子」と「次の兄弟」だけ覚えておくことで、配列を作らずポインタを辿るだけで木全体を巡回できるようにする工夫です。なぜこんな回りくどいことをするのかは、次回のFiber回でじっくり扱います。
(余談ですが、木という「論理的な形」と、それを実際どうメモリに配置するかは別問題です。オブジェクトをポインタで繋ぐのではなく、配列に平坦化してインデックスで管理する実装も存在します。CPUのキャッシュに乗りやすく、ポインタを辿るより速くなることがあるからです。)
図のように、更新前と更新後のツリーで同じ位置にあるノード同士を比較して、変わった部分(この例なら真ん中のli)だけを見つけ出し、そこだけ実DOMに反映します。周りのli要素は「変わってない」と判断されるので、一切触られません。
本当は「全部比較」はものすごく重い
ちょっと待ってください。「新旧のツリーを比較する」と簡単に言いましたが、素直に全部のノードを総当たりで比較しようとすると、実はこれも重い処理です。
一般的な木構造の差分アルゴリズムは、理論上 O(n³) かかることが知られています。要素数が1,000個あったら、10億回オーダーの比較が必要になる計算です。これでは「速くするため」のVirtual DOMが逆に足を引っ張ってしまいます。
この「O(n³)」、実は50年がかりで研究されている問題
さらっと「O(n³)」と書きましたが、この数字の裏にはコンピュータサイエンスのかなりガチな研究史があります。ちゃんと名前もついていて、木の編集距離(Tree Edit Distance)問題と呼ばれます。「2つの木構造を比べて、片方をもう片方に変形するのに最小何回の挿入・削除・書き換えが必要か」を求める、由緒正しいアルゴリズムの研究対象です。
-
1979年、Taiが「The tree-to-tree correction problem」という論文で、この問題が多項式時間で解けることを初めて証明しました。ただし
O(n^6)。理論上解けても実用にはほど遠い速度です。 - 1989年、ZhangとShashaが「Simple fast algorithms for the editing distance between trees and related problems」(SIAM Journal on Computing)で大幅に高速化。以来30年以上、事実上の標準アルゴリズムとして教科書やライブラリで使われ続けています。
-
2007年、Demaine・Mozes・Rossman・Weimannの4人がついに
O(n^3)を達成し、さらに「木を再帰的に分解していく“decomposition”という戦略を使う限り(=知られているほぼ全アルゴリズムが該当)、理論的にこれより速くはできない」ことまで証明しました。React公式ドキュメントが言う「state of the artはO(n³)」は、まさにこの結果を指していると考えられます。 - 2018年、Bringmann・Gawrychowski・Mozes・Weimannはさらに踏み込み、「重み付き版のこの問題は、“APSP予想”(グラフの全点対最短路問題に関する、理論計算機科学で広く信じられている難しさの仮定)が正しい限りO(n³)より真に速くは解けない」という条件付き不可能性証明までやってのけました。
つまりReactの「O(n³)は重すぎる」は、なんとなくの体感ではなく、証明された計算量理論の壁に基づいた話だったわけです。
……で終わっても十分説得力がありますが、実はこの話には続きがあります。2021年、MITのXiao Mao氏が「重みなし版」に限定すれば話は別で、高速行列積の技術を応用してO(n^2.9546)というO(n³)を破る解法があることを示しました("Breaking the Cubic Barrier for (Unweighted) Tree Edit Distance", FOCS 2021 / SIAM J. Computing 2022)。DOMの挿入・削除・書き換えは基本「1回1コスト」の重みなし操作なので、実はこちらの設定の方がDOM diffingには近い話です。
ただし現実的には、この手の高速行列積ベースのアルゴリズムは定数項が絶望的に大きく、ブラウザの中で毎フレーム動かせるようなものではありません。理論上の壁はわずかに崩れても、実務上の壁はほぼそのままだった、というオチです。
なぜ「木」だと計算量が減るのか、しかも「順序」が効いている
ここまでの話は全部「木」に限定した話でした。じゃあ、そもそもなぜ「木」だと話が違ってくるのでしょうか。ここにも明確な理由があります。
木構造には2つの強力な性質があります。
- サイクルがない、再帰的な構造 → 「木全体の比較」を「部分木同士の比較」に分解できる(動的計画法が使える)。文字列の編集距離(レーベンシュタイン距離)を、木に一般化したようなイメージです。
- 兄弟同士に順序がある(今回のDOMの場合) → 子要素同士を「あり得る組み合わせ全部」照合する必要がなく、「左から順に」照合すればいい
この2つ目が、実は見落とされがちですが決定的に効いています。証拠にこんな結果があります。
-
順序ありの木の編集距離 → 多項式時間で解ける(前述の
O(n^3)) - 順序なしの木(兄弟の並び順を区別しない)の編集距離 → NP困難。しかも「ラベルが2種類しかない二分木」というかなり単純な設定でもNP完全であることまで証明されています(Zhang, Statman & Shasha, 1992, “On the editing distance between unordered labeled trees”)
- 木ですらない一般のグラフの編集距離 → こちらもNP困難であることが知られています(Bunke, 1997ほか多数の研究で確認)
「順序を無視していいことにする」だけで、多項式時間からNP困難に転落するわけです。順序があれば、子要素同士の対応付けは「1列に並んだものをどう対応させるか」という文字列比較に近い問題で済みますが、順序を無視すると「あり得る組み合わせの中から最適な対応を探す」という、二部マッチングや部分グラフ同型に近い、組み合わせ爆発する問題になってしまいます。
そして<ul>の<li>要素たちは、まさに順序がある構造です。DOMの子要素には必ず「1番目、2番目、3番目……」という並びがあります。
ここから、key propが存在する本当の意味も見えてきます。keyは単なる「Reactへの親切なヒント」ではなく、この“順序あり”という、計算量を多項式に抑えている前提そのものを補強する仕組みでもあります。位置(=順序)だけで対応付けると、4節で見るkey={index}の事故のように、人間が思う「同一性」とズレることがあります。だからkeyという形で、開発者から「本当の同一性」を追加情報として渡してもらう。DOMが「順序あり」だからこそ「速いけど時々ズレる」ヒューリスティックが成立し、そのズレを埋め合わせるためにkeyがある、という関係です。
Reactは、この土俵で戦うのをやめた
ここまでの話は全部、「任意の2つの木を比較する汎用アルゴリズム」の研究です。汎用である以上、「UIの木構造は普段どう変化しがちか」というドメイン知識を一切使えません。
Reactが賢かったのは、この土俵で世界最速を目指すことを最初から放棄した点です。「任意の木」ではなく「Reactコンポーネントが実際に作る木」という前提を勝手に置いていいなら、話は全く変わります。
- 型が変わるような更新は現実的にはレア → 型が違えば問答無用で作り直していい
- リストの並び替えはよくあるが、各要素の"正体"は
keyで教えてもらえばいい
「同じ土俵でより賢く戦う」んじゃなく「自分に都合のいい土俵を選ぶ」。 Reactが実際にどんな土俵を選んだのか、見ていきましょう。
そこでReactは、2つの割り切ったルール(ヒューリスティック=完璧ではないけど実用上十分な経験則)を導入して、これを O(n) まで落としています。
ルール1:要素の型が違ったら、中身を比較せず丸ごと作り直す
// 更新前
<div><Counter /></div>
// 更新後
<span><Counter /></span>
div が span に変わったら、Reactは中の Counter が同じかどうかなんて考えません。「型が違う=別物」と判断して、古いツリーを丸ごと破棄し、新しいツリーを丸ごと作ります。中身を律儀に比較しない分、判断が一瞬で終わります。
ルール2:同じ階層の子要素は、key で対応関係を教えてもらう
これが次の話につながります。
4. key propで事故る初心者あるある
リストを描画するとき、こう書いたことがあるはずです。
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
))}
動きます。エラーも出ません。でも、これ実は事故りやすい書き方です。
何が起きるか
key={index} にすると、Reactは「配列の0番目の要素」「1番目の要素」……という位置で新旧を対応付けます。中身が何であるかは見ていません。
ここでリストの先頭に新しいTODOを追加したとします。
追加前: [牛乳を買う(key=0), 卵を買う(key=1)]
追加後: [パンを買う(key=0), 牛乳を買う(key=1), 卵を買う(key=2)]
人間からすると「パンを買うが1個増えただけ」に見えますが、Reactの目線では違います。
-
key=0:中身が「牛乳を買う」→「パンを買う」に変わったと判定 -
key=1:中身が「卵を買う」→「牛乳を買う」に変わったと判定 -
key=2:対応する前の要素がないので新規作成
本当は「パンを買う」1件が増えただけなのに、既存の2件まで「中身が変わった」扱いになり、無駄な更新が発生します。さらに厄介なのは、各li要素にinput(編集フォームなど)やチェックボックスの状態を持たせている場合です。DOM要素自体は使い回されるので、中身のテキストだけ更新されて、フォームの入力内容やチェック状態は古いまま残る、みたいな事故が起きます。
直し方
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
todo.id のような、その項目固有で、並び替えても変わらない値をkeyにすれば、Reactは「あ、これは同じ『牛乳を買う』のやつだ、位置が動いただけだ」と正しく認識できます。図の下半分のように、新しく増えた「パンを買う」だけが「追加」として扱われ、他は「移動しただけ」で済みます。
「配列のindexをkeyにするな」とよく言われるのは、こういう理由からでした。
5. でも、Virtual DOMは万能じゃない
ここまで「Virtual DOMは賢い」という話をしてきましたが、正直に言うと、これは銀の弾丸ではありません。
Virtual DOMにも当然コストがあります。
- 新しいツリーを毎回JSオブジェクトとして作る(メモリ確保)
- 新旧ツリーを比較する(CPU時間)
- 差分をまとめて実DOMに適用する
もしアプリがものすごくシンプルで、更新箇所が最初から分かりきっているなら、人間が手でピンポイントにDOMを1箇所だけ書き換えるほうが、Virtual DOMを経由するより速いことすらあります。比較するという工程そのものが、ゼロコストではないからです。
じゃあ、「Virtual DOMだから速い」って言葉は何だったのか。正確に言うとこうです。
Virtual DOMは「最速」を実現する仕組みじゃなくて、「開発者が手動で最適化しなくても、そこそこ速い」を実現する仕組み
素朴に書いたVanilla JSのコード(レイアウトスラッシングが起きるようなもの)と比べれば、Virtual DOMは十分速い。でも、ガチガチにチューニングされた手書きDOM操作と比べたら、Virtual DOMのほうが遅くなる場面もある。ここが「半分正解で半分ウソ」の意味でした。
おまけ:そもそも「木」という見方は万能なのか
ここまで「UIは木構造として比較するものだ」という前提で話を進めてきました。最後に一歩引いて、この前提自体を疑ってみます。
DOMの「入れ子構造」を表現する分には、木は自然で正しいモデルです。ボタンはdivの中にあり、そのdivはさらに外側のdivの中にある——この包含関係は現実に階層的なので、木で表現して何の無理もありません。
でも、UIが扱う「関係」は入れ子構造だけではありません。「どのstateが、どの表示に影響するか」という依存関係は、実は木の形をしていないことがよくあります。
たとえば、ログイン中のユーザー名をヘッダーにもフッターにもサイドバーにも表示したいとします。この3箇所はコンポーネントツリー上では全く別の枝に属しているかもしれませんが、同じ1つのstateに依存しています。これは木ではなく、1つの頂点(state)から複数の離れた頂点(表示箇所)へ矢印が伸びる、グラフの形です。
ReduxやContext、そして次で話すSignalsが存在する理由は、まさにここにあります。「propsをツリーに沿ってバケツリレーする」というツリー由来の方法だけでは、こういうグラフ状の依存関係をうまく表現できません(いわゆる"prop drilling"のつらさです)。だから別チャネルとして、木の形に縛られない依存関係の管理方法が必要になります。
面白いのは、この「グラフの編集距離」もまた一般にはNP困難だということです。もしReactが「stateの依存関係グラフ」まで丸ごと差分検出しようとしたら、木の比較どころではない計算量になっていたはずです。だからこそ、「見た目の入れ子(木)」と「stateの依存関係(グラフ)」は、意図的に別々の仕組みで扱われています。Reconciliationは木を、この後見るSignalsはグラフを、それぞれ得意な形で処理しているわけです。
6. もっと効率よくできないか?→ "比較しない" という発想
ここまでの話を一歩引いて見ると、Virtual DOMがやっていることは、結局こういうことです。
何が変わったか分からない
↓ だから
とりあえず全部作り直して、比較して、探す
賢いヒューリスティックで比較コストを削ってはいますが、「探す」という行為そのものは残っています。
ここで、もし発想を逆転できたらどうでしょうか。
「何が変わったか分からないから探す」んじゃなくて、
「最初から、どのstateがどのDOMノードに対応しているか知っておけばいい」
これが、SolidJSやSvelteが採用している Signals(シグナル) という仕組みの発想です。
Virtual DOM方式では、stateが変わるとコンポーネント関数がまるごと再実行され、新しいツリーが作られ、それを古いツリーと比較して、ようやく実DOMに反映されます。
Signals方式では、state(Signal)が変わった瞬間、「このSignalを使っているのはこのDOMノードだけ」という紐付けが最初からされているので、比較というステップを経由せず、そのノードだけを直接書き換えます。コンポーネント関数自体、再実行すらしません。
イメージで言うとこうです。
- Virtual DOM:模様替えのたびに部屋全体の写真を撮って、前回の写真と見比べて、違うところを直す
- Signals:動いた家具にGPSをつけておいて、動いたらその家具だけ直接動かす
この発想、実は目新しくない
「値が変わったら、依存する部分だけ再計算する」というアイデア自体は、UI業界的には結構古参です。表計算ソフトを思い出してください。ExcelでA1セルを書き換えたら、A1を参照しているB1だけが再計算されますよね。シート全部を再計算し直したりはしません。あれもまさに「どのセルがどのセルに依存しているか」を最初から追跡している、Signalsと同じ発想です。1979年のVisiCalcの時代からある、由緒正しいアイデアなんです。
これをプログラミング言語の理論として体系化する試みも、実はずっと続いてきました。
- Conal ElliottとPaul Hudakが1997年に発表した「Functional Reactive Animation」(ICFP)は、「連続的に変化する値」と「離散的なイベント」を関数型プログラミングの枠組みで扱う**FRP(Functional Reactive Programming)**の出発点になった論文です。「時間とともに変わる値」を第一級の値として扱う、という発想はここに端を発しています。
- Umut Acarは2005年の博士論文以降、**自己調整計算(Self-Adjusting Computation)**という分野を切り開きました("Imperative Self-Adjusting Computation", POPL 2008 など)。「入力が少し変わったとき、依存関係を追跡しておいて、影響を受ける部分だけ再計算する」ことを、プログラミング言語のレベルで一般化する研究です。
つまりSignalsは、どこかのフレームワークが突然発明したものではなく、表計算ソフトの実務的な知恵と、20〜30年分のプログラミング言語研究が合流した先にある仕組み、と捉えると腹落ちしやすいです。
実際どうやって「依存関係」を知るのか
ここが一番気になるところだと思うので、仕組みを簡略化して覗いてみます。
// 超簡略化した signal の実装イメージ(実際のSolidJSはもっと複雑です)
let currentSubscriber = null; // 「今、実行中の関数」を覚えておくグローバル変数
function createSignal(initialValue) {
let value = initialValue;
const subscribers = new Set();
function read() {
if (currentSubscriber) {
subscribers.add(currentSubscriber); // 読まれた瞬間、勝手に購読登録される
}
return value;
}
function write(newValue) {
value = newValue;
subscribers.forEach((sub) => sub()); // 購読者だけに変更を通知
}
return [read, write];
}
function createEffect(fn) {
const run = () => {
currentSubscriber = run; // 「今動いてるのは自分だ」と名乗る
fn(); // 実行中にsignalのreadが呼ばれたら自動で購読される
currentSubscriber = null;
};
run();
}
ポイントは、「読む(read)」という行為そのものが、購読登録を兼ねているところです。開発者が明示的に「このstateとこのDOMノードを紐付けます」と宣言する必要はありません。ただsignal()を呼んで値を読むだけで、勝手に依存関係グラフが出来上がっていきます。ReactのuseEffectの第2引数(依存配列)を手動で書かなくていいのは、この仕組みのおかげです。
もちろん実際のSolidJSはこんなに単純ではなく、条件分岐で読まれなくなったsignalの購読を解除する処理(クリーンアップ)や、値をキャッシュするcreateMemo、複数の更新をまとめるバッチ処理なども実装されています。それでも根っこの仕組みは、この「読んだら自動購読、書いたら通知」というシンプルな発想です。
簡単そうに見えて、実は難しい問題もある
このやり方、素朴に実装すると1つ厄介な問題が起きます。グリッチ(glitch)、あるいはダイヤモンド問題と呼ばれるものです。
A(state)が変化
├─ B(Aに依存)が再計算される
└─ C(Aに依存)が再計算される
↓
D(BとCの両方に依存)はどのタイミングで再計算されるべき?
Aが変化したとき、素朴に「通知されたらすぐ再計算」を実装すると、Dは「Bだけ新しい値・Cはまだ古い値」という一瞬だけ矛盾した中間状態で1回計算されてしまい、その直後にCの更新が届いてもう1回計算し直す……ということが起こり得ます。一瞬とはいえ矛盾したデータで副作用(DOM更新やAPI呼び出しなど)が走ってしまったら、バグの元です。
実際のSignals実装は、この問題を避けるために通知を2段階に分けています(push-pullモデルと呼ばれます)。まず「変わった可能性がある」というフラグだけを依存グラフ全体に伝播させ(push)、実際に値を使うタイミングで依存元を遡って本当に必要な分だけ再計算する(pull)。これによりDが計算される頃には、BもCも確実に最新の値になっていることが保証されます。地味ですが、正しさに直結する重要な工夫です。
SolidJSやSvelteだけの、一過性の流行りじゃない
「結局マイナーな2フレームワークの話でしょ?」と思うかもしれませんが、この数年で状況が変わってきています。
- Svelteは4以前、「コンパイラがコードを解析して、代入文を検出したら再描画」という別方式でしたが、2024年10月にリリースされたSvelte 5で「Runes」という仕組みを導入し、内部実装をSignalsベースに刷新しました。
- さらに大きいのが、TC39(JavaScript言語仕様を決める委員会)に提出されている“Signals”提案です。Angular・Vue・Preact・Qwik・SolidJS・Ember・MobXなど、主要フレームワークの開発者たちが共同で設計に関わっており、「フレームワークごとに車輪の再発明をするのはもうやめて、JavaScript本体にSignalsを組み込もう」という動きです。2025年後半にStage 2まで進み、Stage 3に向けて議論が続いています。
「一部のフレームワークの変わった思想」ではなく、JavaScriptのUI開発全体が向かいつつある方向、と捉えたほうが実態に近そうです。
もちろんSignalsにも欠点はあります。「どのstateがどこで使われているか」を正確に追跡する仕組みが必要で、依存関係の管理はVirtual DOMより神経を使います。エコシステムやツールの成熟度も、まだReactに及ばない部分があります。銀の弾丸はどこにもありません。
でも、「比較して探す」コストを消し去るという発想自体は、覚えておいて損はないです。
この「stateとDOMを直接つなぐ」仕組みを実際に自分の手で作ってみるのが、Hooks回(第4〜5回)でやることです。 中身の原理を知っていれば、いつか自分たちで似たような仕組みを組み立てられるようになります。
まとめ
| 直接DOM操作 | Virtual DOM(React) | Signals(Solid / Svelteなど) | |
|---|---|---|---|
| 更新の判断方法 | 自分で全部書く | 新旧ツリーを比較(diff) | state変化を直接DOMに紐付け |
| 比較コスト | なし(その代わり書き漏れリスク) | ヒューリスティックでO(n) | 基本的に不要 |
| 得意なこと | 極小規模・ピンポイントな更新 | 複雑な画面でも書き漏れが起きにくい | 比較コストなしで高速更新 |
| 苦手なこと | 機能が増えると管理が破綻する | 比較コスト自体はゼロにできない | 依存関係の管理を丁寧にやる必要 |
「Virtual DOMだから最速」ではなく、「Virtual DOMだから、開発者が気をつけなくてもそこそこ速い」。そして、その先には「そもそも比較しない」という発想もある。ここまで理解できれば、「なんとなくReact使ってます」から一歩抜け出せたはずです。
参考文献
木の編集距離(Reconciliationまわり)
- Tai, K. (1979). The tree-to-tree correction problem. Journal of the ACM, 26(3).
- Zhang, K., & Shasha, D. (1989). Simple fast algorithms for the editing distance between trees and related problems. SIAM Journal on Computing, 18(6).
- Zhang, K., Statman, R., & Shasha, D. (1992). On the editing distance between unordered labeled trees. Information Processing Letters, 42(3).
- Demaine, E. D., Mozes, S., Rossman, B., & Weimann, O. (2007). An optimal decomposition algorithm for tree edit distance. ICALP.
- Bringmann, K., Gawrychowski, P., Mozes, S., & Weimann, O. (2018). Tree edit distance cannot be computed in strongly subcubic time. SODA.
- Mao, X. (2021). Breaking the cubic barrier for (unweighted) tree edit distance. FOCS.
- Bunke, H. (1997). On a relation between graph edit distance and maximum common subgraph. Pattern Recognition Letters, 18(8).
- Pawlik, M., & Augsten, N. (2016). Tree edit distance: Robust and memory-efficient. Information Systems, 56.
- Bille, P. (2005). A survey on tree edit distance and related problems. Theoretical Computer Science, 337(1-3).
- React公式ドキュメント「Reconciliation」
Signalsまわり
- Elliott, C., & Hudak, P. (1997). Functional reactive animation. ICFP.
- Acar, U. A. (2005). Self-Adjusting Computation(博士論文, Carnegie Mellon University).
- Acar, U. A., Ahmed, A., & Blume, M. (2008). Imperative self-adjusting computation. POPL.
- TC39 Signals Proposal(github.com/tc39/proposal-signals)
- Svelte公式ブログ「Introducing runes」
次回
**Fiber ~ なぜReact 16でアーキテクチャが刷新されたのか ~**
- 差分計算、実は「一気に」やると画面がカクつく問題があった
- Reactが「作業を中断・再開できる」仕組みに作り替えた理由
- 優先度をつけてレンダリングするってどういうこと?
を書きます。






