Vue Vapor Mode:把 virtual DOM 蒸發掉的編譯策略
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(7)
1. Outline
- 起點 · Vapor Mode is the Future of Vue
從「不改程式碼就變快」的承諾出發,先攤開 Vue 現行 virtual DOM 的完整迴圈與編譯期最佳化,指認出削不掉的固定成本,再用一句反問推出 Vapor Mode,最後用 SFC Playground 的編譯輸出對照把理論落成看得見的程式碼。
2. YouTuber 的思維推導
Vapor Mode is the Future of Vue
LearnVue · 4m08s · 字幕 en · vision=on (使用者指定 vision=true;且作者多次指著畫面說「let's take a look at the build code」「here we have a bunch of n number」「even looking at this code」,編譯結果的對照必須看畫面才懂。) · YouTube
預估 vs 實際耗時
| 階段 | 預估 | 實際 |
|---|---|---|
| fetch | 6s | 1s |
| segment | 38s | 3m00s |
| shot | 22s | 3s |
| analyze | 3m45s | 10m37s |
| render | 5s | – |
作者的推導是一條「先立承諾、再拆成本、最後還原機制」的鏈。他從一個誘因出發:不改任何一行程式碼就讓 app 更快、更省記憶體。但他很清楚這個承諾要能被相信,必須先讓聽眾知道現在的 Vue 把時間花在哪裡,所以他倒回去把 Vue 的執行路徑完整攤開——template 在 build time 編譯成 render function,render function 產生 virtual DOM tree,再用真實 DOM 指令掛上頁面;資料一變,整條路徑重走一次,新舊兩棵樹 diff 之後 patch 回真實 DOM。
接著他做了一個關鍵的鋪墊:先替 Vue 說好話,指出因為多了編譯這一步,Vue 能做 static hoisting 這類 runtime 框架做不到的最佳化。這一步表面上是誇 Vue,實際上是為了讓下一步的反面論證更有力——連做了編譯期最佳化的 virtual DOM 都還有削不掉的固定成本:要佔記憶體、資料一變就要重建一堆 v-node、要走完整棵樹找差異。成本落點確定後,他用一句反問把前提整個換掉:如果資料改變時我們早就知道 DOM 的哪幾個位置會受影響呢?
那麼 diff 就是在重算一個已知的答案,於是不只不用重跑 render function、不用比對,連那棵 virtual DOM tree 本身都可以不要。這就是 Vapor Mode。但這個結論押在「早就知道哪裡會變」這個前提上,所以他立刻把前提還原成 Vue 既有的 reactivity system:同一套追蹤機制,只要把訂閱的粒度從整個元件的 render function 縮小到單一 DOM 節點,前提就成立,不需要任何新發明。
理論齊了之後他換成證據模式,用 SFC Playground 把同一份 App.vue 在 VAPOR OFF 與 VAPOR ON 下的編譯輸出並排看:一邊是 _createElementVNode 組出來的樹,一邊是 n0、n1 這些真實節點變數加上 _renderEffect 直接寫文字,前面所有說法在這裡從示意圖變成看得見的程式碼。最後他從技術回到工程現實:怎麼落地(完全 opt-in、以元件為單位、可與現有元件混用)、好處的兌現條件(整包都是 Vapor 元件才能把 vdom runtime 從 bundle 移除)、以及還沒有答案的部分(Vapor 與非 Vapor 元件的功能對等程度、只支援 Composition API 與 script setup 之外還有什麼限制),把結論停在「值得放進雷達」而不是「現在就換」。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- Vapor Mode 的承諾 → 作者把「不用 virtual DOM」當成好處的來源丟了出來,卻還沒說 virtual DOM 現在到底在做什麼、為什麼它會變成成本。要檢驗這張欠條,下一步必須先把現在的 Vue 從 template 到畫面的完整路徑攤開來看。
- Vue 現在怎麼運作:vdom 全迴圈 → 整條迴圈已經攤開,但「compiler-informed」的那半個名字還懸著沒解釋。作者留下的線索是一個問題:既然 Vue 多了一道把 template 編譯成 render function 的手續,這道手續除了轉換格式之外,還能拿來做什麼?
- 編譯期最佳化:static hoisting → 作者已經把「編譯期知道得比 runtime 多」立成事實,但他用的是防守姿態(Vue 比 React 做得多)。留給下一段的是一個他還沒問出口的問題:編譯期最佳化只削掉了模板裡靜態的那一半,剩下真的會變的那一半,還是得走一次完整的建樹與比對——這筆帳能不能也省掉?
- vdom 的代價,與拿掉它的念頭 → 結論已經下了,但它整個押在一個尚未兌現的前提上:「資料變的時候,我們早就知道哪些 DOM 會受影響」。作者到這裡一個字都還沒說 Vue 憑什麼知道。這個空缺就是下一段必須補上的。
- 做得到的原因:reactivity system → 原理到這裡齊了,但整條推理至今全部停留在白板方塊與四行偽程式碼上。作者自己把下一步指了出來:repo 是公開的、還有一個 playground——線索很具體,就是「同一個元件,把 Vapor 開關切一下,編譯出來的程式碼到底長什麼樣」。
- Playground 對照:關掉與打開 Vapor → 機制與證據都齊了。但這兩份輸出差異之大,本身就丟出了下一個問題:長成這樣的程式碼,要怎麼跟現有那些已經寫好的元件在同一個 app 裡共存?作者留下的線索是他這段結尾那句「即使不完全看懂也覺得合理」——技術上說得通之後,接著該問的是它怎麼被放進真實專案裡。
- 整合計畫、限制與結語
3. 逐段說明
Vapor Mode is the Future of Vue
1. Vapor Mode 的承諾 0:00–0:20
作者開門見山丟出一個誘人的假設:不改任何一行程式碼,就讓 app 更快、記憶體用得更少。這就是 Vue Vapor Mode——Vue 團隊正在開發中的實驗性編譯策略,靈感來自 Solid JS。關鍵在於:元件寫法完全不變,但跑起來不再使用 virtual DOM。他接著立下這支影片要回答的三個問題:它到底怎麼運作、能帶來什麼好處、目前開發到哪了。
推理因為前情提要已經確立聽眾是「會寫 Vue、但沒讀過 runtime 原始碼」的人,作者選擇先給結果再給原理:他先丟出一個可以被檢驗的承諾(更快、更省記憶體、而且程式碼不用改),再立刻把它拆成三個問題——它到底怎麼運作、能帶來什麼好處、目前開發到哪了。這三個問題就是整支影片的骨架,也讓聽眾隨時知道現在這一段是在回答哪一題。
AI 補充有一個詞值得先咬清楚:作者說 Vapor 是一個 compilation strategy,不是新 API、不是新語法。改的是「同一份 template 被編譯成什麼樣的 JavaScript」,所以「不用改程式碼」不是行銷話術,而是這個設計方式的直接後果——你的原始碼是編譯器的輸入,輸入不變、輸出換一套,這在邏輯上是成立的。 他提到的靈感來源 Solid JS 也值得補一句:Solid 從第一天就沒有 virtual DOM,它用編譯把 JSX 直接變成建立與更新真實 DOM 的程式碼。Vapor 等於是把這套已經被驗證過的做法,搬進 Vue 既有的 SFC 寫法裡——這也是為什麼作者敢說寫法不用變。 最後要指出作者在這裡跳過的一步。他說「執行時不使用 virtual DOM,因此我們得到各種好處」,但「不用 virtual DOM 為什麼就會有好處」他一個字都還沒說。這是一張欠條,先記著,整支影片有沒有說服力,就看這張欠條兌不兌得出來。
2. Vue 現在怎麼運作:vdom 全迴圈 0:20–0:56
要懂 Vapor,得先懂現在的 Vue。作者說 Vue 用的是 compiler-informed virtual DOM,先解釋 virtual DOM 這一半:Vue 在 build time 把每個元件的 template 編譯成 render function,執行時配上資料產生 virtual DOM tree,再用真正的 DOM 指令掛上頁面。之後只要 reactive data 一變,render function 就重跑、產生新的 tree,新舊兩棵樹做 diff,最後把差異 patch 回真實 DOM。



推理因為上一段只給了承諾沒給機制,所以作者接著把 Vue 的執行路徑拆成一條有向的鏈:template 在 build time 被編譯成 render function,render function 配上資料產生 virtual DOM tree,再用真實 DOM 指令 mount 成頁面上的元素。然後他讓 reactive data 從下方接回 render function,把直線變成迴圈:資料一變,render function 重跑、產生新樹、新舊樹 diff、差異 patch 回真實 DOM。他刻意先講完整條迴圈才談成本,因為「成本落在哪一段」得先有這張圖才指得出來。
- Ctemplate 編譯成 render function,資料變動觸發重跑、新舊樹比對後套用到真實 DOM→ 畫地圖
- Ctemplate 編譯後得到的函式,執行它會產生當下該有的 virtual DOM tree→ 畫地圖
- Rtrack 與 trigger 分別做什麼→ 存+回想
- AVue 一直知道該重畫哪個元件,只是不知道元件裡哪一塊要重畫→ 批判類比
- E白板圖:五方塊直線加 Reactive Data 用 track/trigger 接回形成迴圈→ 存+演練
- P手繪一次自己寫過的元件的 track/trigger 渲染迴圈→ 練習
AI 補充畫面上是一張白板圖,比作者唸出來的內容多給了兩個字,值得停下來看:五個方塊由左到右是 Template →compile→ Render Function →returns→ Virtual Dom Tree →mount/patch→ Actual DOM,而 Reactive Data 掛在下方,用兩條邊接回 Render Function,一條標著 trigger,一條標著 track。 track 這條邊是作者沒說、但整條迴圈少不了的一步:render function 執行的時候,Vue 會記錄它讀到了哪些 reactive data;之後只有那些被讀過的資料變動,才會 trigger 重跑。也就是說 Vue 從來不是「有東西變就全部重畫」,它一直知道「哪個元件該重畫」——只是還不知道「元件裡的哪一塊該重畫」。這個「知道一半」的狀態,是理解後面整條推理的關鍵。 圖上還有一格 `h('div', /*... */)`,那就是 render function 的長相。render function 不回傳 HTML 字串,它回傳一串 h() 呼叫的結果,也就是一棵純 JavaScript 物件組成的樹——這棵樹就是 virtual DOM tree 的實體。左上角那份範例 template(一個 div 裡有 `<span>{{ val }}</span>` 和一個寫死 hi 的 div)會被反覆拿來當例子,記住它會省事很多。 最後,作者說 Vue 有的是 compiler-informed virtual DOM,然後說「我們先談 virtual DOM 這半」——他明說了要把名字的另一半懸著。
3. 編譯期最佳化:static hoisting 0:56–1:27
接著解釋名稱裡「compiler-informed」的那一半,也是 Vue 目前勝過 React 的地方:因為多了「把 template 編譯成 render function」這一步,Vue 可以在編譯期做最佳化。作者舉 static hoisting 為例——template 裡永遠不變的部分,例如寫死 Hi 的那個 div,不會在每次 re-render 都產生新的 virtual node,而是只建立一次,runtime 也知道它永遠不變、不必去 diff 它。這只是其中一項最佳化。

推理因為上一段建立了「template 會先被編譯成 render function」這一步,所以作者接著指出這一步是 Vue 相對於純 runtime 方案的結構性優勢:編譯器手上有整份 template 的靜態文字,可以事先把內容分成「死的」與「活的」兩類。他只舉 static hoisting 一個例子,而且明說「這只是其中一項」、把完整清單丟到說明欄——因為他真正要建立的不是「Vue 有幾項最佳化」,而是「編譯期知道得比 runtime 多」這個觀念。這個觀念一旦成立,後面的推論才有立足點。
- C編譯器把模板中永遠不變的片段提到 render function 外面,只建立一次→ 畫地圖
- Rstatic hoisting 省的是哪一側的成本→ 存+回想
- E節點樹範例:動態 {{val}} 標綠色,靜態 "hi" 整支框起來只建一次→ 存+演練
AI 補充這一段的截圖把上一段那份範例 template 畫成了節點樹,而且用視覺直接標出編譯器的分類結果:根節點 div 分成兩支,左邊是 p → span → `{{ val }}`,最底下那個 `{{ val }}` 是綠色的;右邊是 div → 文字 "hi",整支被一個框框起來。綠色代表動態、框起來的代表被 hoist 出去。也就是說,這棵樹在編譯完成的那一刻就已經被塗上顏色了,runtime 只要照著顏色做事。 有一個區分作者沒講,但不講清楚很容易誤會:static hoisting 省掉的是「重新建立 virtual node 物件」和「比對它」的成本,不是省掉真實的 DOM 元素。頁面上那個 `<div>hi</div>` 一樣存在、一樣佔記憶體,被提出去只建一次的是它在 JavaScript 那邊的分身。這個區分之後在估算「拿掉 virtual DOM 到底省下什麼」時很關鍵——省的一直是分身那一側的成本。 另外他說的「其他最佳化」不是虛指,patch flag、block tree 都屬於這一類:全部都是同一個套路——編譯器把它看出來的事實編碼進輸出,讓 runtime 不必在執行期重新推導一次。
術語:compile-time optimization
「that tools like react don't or at least don't do yet」
4. vdom 的代價,與拿掉它的念頭 1:27–2:01
推理的轉折點。作者指出就算有編譯器幫忙,virtual DOM 仍有無法消除的 overhead:它得佔記憶體,資料一變就得重建一堆 v-node,還得走完整棵樹找差異。於是他提出反問——如果 reactive data 改變時,我們早就知道 DOM 的哪幾個位置會受影響呢?那就可以直接雷射般打進那個點改掉它,不必重跑整個 render function、不必 diff 整棵樹,甚至根本不需要這棵 virtual DOM tree。他用 val 改變只更新那個 span 為例:這就是 Vue Vapor Mode。


推理因為上一段證明了編譯器能事先知道很多事,所以作者接著把這個能力推到極限:如果編譯器連「哪個資料影響哪個 DOM 節點」都知道,那 diff 就是在重新計算一個早就已知的答案。他的論證分成三拍——先列出最佳化削不掉的固定成本(要佔記憶體、資料一變就要建一堆 v-node、要走完整棵樹找差異),再用一句反問把前提整個換掉(如果我們早就知道哪裡會變呢),最後讓結論自己掉出來:不只不用重跑 render function、不用 diff,連這棵樹本身都不需要。這是整支影片的轉折點,而它的說服力完全建立在前兩段先把迴圈畫清楚上。
- C為了維持 virtual DOM 這層中介必須付出、無法被編譯期最佳化消除的固定成本→ 畫地圖
- Ebefore/after 兩張圖:Virtual Dom Tree 方塊整個從圖上消失→ 存+演練
- RDOM command 的定義→ 存+回想
AI 補充這兩張截圖是同一張圖的 before / after,資訊比作者唸出來的更精確,值得逐格看: 第一格裡,上一段那張流程圖被加了註記——Virtual Dom Tree 方塊的正上方多了一個時鐘圖示和一個像磁碟/堆疊的圖示,正是他口中「要花時間走訪」和「要佔記憶體」這兩筆帳,被畫在同一個方塊頭上;左下角多了「Compile Time Optimizations」的字樣,用一條線指向 compile 那一段,等於承認前一段講的最佳化只作用在這條邊上;而 Reactive Data 這時多拉出一條虛線,越過中間所有方塊直接接到 Actual DOM。那條虛線就是他正在提出的新路徑,此刻還與舊路徑並存。 第二格是決定性的一格:Virtual Dom Tree 這個方塊從圖上消失了。剩下的只有 Render Function —mount→ Actual DOM,以及 Reactive Data —patch(虛線)→ Actual DOM。這張圖把 Vapor 的定義說得比任何一句話都清楚:mount 仍然要走一次,畫面總得先被建立出來;被拿掉的是之後每一次更新的中介——更新時資料直接 patch 到真實 DOM,中間沒有任何一棵樹。 順著這張圖可以補上作者第一句承諾的來源。省下的不只是 diff 的 CPU 時間,還有那些短命 v-node 物件的配置與回收——這就是「use less memory」那半句的出處,也正是圖上那個磁碟圖示標的東西。 另外「不需要重跑整個 render function」這句要小心讀。Vapor 不是沒有初始化程式碼,它一樣有一次性的建立階段;被取消的是「每次更新都重跑一個會產生整棵樹的函式」這個習慣動作。
術語:vdom overheadfine-grained updateDOM command
5. 做得到的原因:reactivity system 2:01–2:19
為什麼 Vue 有本錢拿掉 virtual DOM?作者的答案是 Vue 既有的 reactivity system:可以讓 DOM 的一小塊直接訂閱某個 reactive state,那個 ref 一變,就只有這一小塊需要更新。他接著說 Vapor Mode 仍在開發中,但 repo 已公開,還有一個 playground 可以玩,於是帶出下一段的實例。

推理因為上一段的結論完全押在那個前提上,所以作者接著把前提還原成既有機制:reactivity system 一直在追蹤「誰讀了這個 ref」,只是在現在的 Vue 裡,被登記的訂閱者是整個元件的 render function。他要主張的其實只有一件事——把訂閱的單位從「元件」縮小到「單一 DOM 節點」,上一段那條從資料直達真實 DOM 的虛線就成立了。這個推理的漂亮之處在於它不需要任何新技術,只需要換一個訂閱粒度,也因此他能很自然地接著說「repo 已經公開、還有 playground」,把話題從原理帶向證據。
- C資料變動時只更新真正受影響的那一個 DOM 節點,不重算也不比對其他部分→ 畫地圖
- CVue 追蹤誰讀了資料、資料變動時通知它們的整套機制→ 畫地圖
- Rref 與 effect 的定義→ 存+回想
- A訂閱粒度從整份元件縮小到單一 DOM 節點→ 批判類比
AI 補充這一段的截圖是一張只有四行的投影片,而且作者特地在第一行寫了「不是真的程式碼,但概念類似」: ```js watch([myDep], () => { node.nodeValue = myDep.value }) ``` 這四行把整個 Vapor 的心智模型壓縮完了:一個訂閱綁著一個 ref,被觸發時做的事就是把某個真實 DOM 節點的文字直接寫掉。沒有樹、沒有比對、沒有回傳值可言。他用 watch 只是為了讓人看得懂形狀。 真正要盯住的是 `node` 這個變數。它是一個已經拿在手上的真實 DOM 節點參照——而「這個參照從哪裡來」,正是 Vapor 整套設計裡最硬的工程問題:編譯器必須在編譯期替模板裡每一個會變動的位置,產生一個可以穩定拿到的節點參照。作者在這裡沒有提,但這是把偽程式碼變成可用編譯器的關鍵一步。 還有一點值得補:這一段真正的主張是「粒度」,不是「機制」。同一套 reactivity,粒度粗(元件層級)就必須靠 diff 事後找出實際變的地方;粒度細(節點層級)就不需要中介。這也正是 Solid JS(見第 1 段)一直以來的做法——所以 Vapor 借的不是實作,是這個訂閱單位的選擇。
術語:effect
6. Playground 對照:關掉與打開 Vapor 2:19–3:14
作者用 playground 的三個小例子(可切換的 H1、綁 input 的訊息、計數器)比對編譯結果。Vapor 關閉時:一個 setup function 建立 refs,加上一個 render function,資料一變就整個重跑,用 createElementBlock、createElementVNode 這類函式組出 virtual DOM tree。Vapor 打開後:setup function 還在,但回傳的不是 render function,而是一個自執行函式;裡面是一堆 n 開頭的變數,用 HTML 字串和 template 方法建立真正的 DOM 節點,v-if 用 watcher 監看 show.value,動態文字用 effect 綁在對應的 ref 上,再加上處理 directive 與事件的 helper。作者說即使不完全看懂這段程式碼,它也很合理:UI 的每一塊都精準地由影響它的那個 ref 控制。




推理因為前面所有推論都停在示意圖層級,所以作者選了最省力也最有說服力的驗證方式:同一份 App.vue,只切 VAPOR OFF / VAPOR ON 這一顆開關,把兩邊的 JS 輸出並排看。他挑的範例也不是隨手寫的——一個用 v-if 切換的 h1、一個用 v-model 綁 input 的訊息、一個 counter,剛好蓋住條件渲染、雙向綁定、事件與動態文字這四種最常見的動態。只要這幾種在 Vapor 輸出裡都找得到對應的機制,前面的推理就算落了地。
- EPlayground 對照:關閉 Vapor 時 setup 回傳 render function 與 createElementVNode→ 存+演練
- Rpatch flag 的定義→ 存+回想
- RrenderEffect/event delegation/template cloning 各做什麼→ 存+回想
AI 補充這是全片資訊密度最高的一段,四張截圖各自扛著不同的證據,值得逐張讀。 **關閉 Vapor(第一張)**:右側輸出是 `const __sfc__ = { __name: 'App', setup(__props) { … } }`,setup 裡是原封不動的三個 ref 與 increment 函式,接著 `return (_ctx, _cache) => { return (_openBlock(), _createElementBlock(_Fragment, …` ——setup 回傳的是一個函式。這一格就是第 2 段那張圖裡「Template →compile→ Render Function」的實體,抽象方塊在這裡變成了看得見的程式碼。 **關閉 Vapor、往下捲(第二張)**:`_createElementVNode("button", { onClick: … }, "Toggle H1")`、`(show.value) ? (_openBlock(), _createElementBlock("h1", _hoist…)) : _createCommentVNode("v-if", true)`、`_withDirectives(… "input" …, 512 /* NEED_PATCH */, [[_vModelText, msg.value]])`,結尾是 `], 64 /* STABLE_FRAGMENT */)`。這裡有三件事值得指出來:其一,v-if 為 false 時輸出的不是「什麼都沒有」,而是一個註解節點佔位——因為 diff 需要新舊兩棵樹的位置對得上,位置不能憑空消失。其二,`_hoist…` 就是第 3 段講的 static hoisting,真的出現在輸出裡。其三,`512 /* NEED_PATCH */`、`64 /* STABLE_FRAGMENT */` 這些數字就是第 3 段說的「其他最佳化」的實體,它們是編譯器留給 runtime 的提示,告訴它哪些部分可以跳過不比。同時,這一整片 `_create*` 呼叫也讓 vdom 的成本有了具體形狀:每一次更新,這些呼叫全部要再跑一遍。 **打開 Vapor(第三張)**:setup 一字不差還在,但 return 的不再是 `(_ctx, _cache) =>`,而是 `(() => { … })()` 這個自執行函式,裡面是 `const n0 = t0()`、`const n6 = t2()`、`const n4 = n6.firstChild`、`const n5 = n4.nextSibling`。這正是第 5 段那個 `node` 變數的真身:`t0`、`t2` 是用 HTML 字串一次做出來的模板、呼叫時 clone 一份,而個別節點的參照不是一個一個 createElement 出來的,是靠 `firstChild` / `nextSibling` 在剛 clone 出來的結構上走出來的——這比逐一建立元素便宜得多。作者說「一堆 n 開頭的變數,我猜是不同的節點」,猜對了。 **打開 Vapor、往下捲(第四張)**:`_withDirectives(n4, [[_vModelText, () => msg.value]])`、`_delegate(n0, "click", …)`、`const n1 = _createIf(() => (show.value), () => { const n3 = t1(); _renderEffect(() => _setText(n3, msg.value)); return n3 })`、`_delegate(n5, "click", () => increment)`、`_renderEffect(() => _setText(n5, " Increment ", count…))`,最後 `return [n0, n1, n6]`。那行 `_renderEffect(() => _setText(n3, msg.value))` 就是第 5 段那四行偽程式碼的實作,形狀幾乎一模一樣。 作者有兩處說法可以再精確一點。他說 v-if「用的是類似 watcher 監看 show.value 的東西」,實際輸出是 `_createIf(() => show.value, …)`——它是一個分支原語,除了監看之外還要負責子節點區塊的建立、插入與銷毀,責任比 watcher 重。他說動態文字「用 effect 綁在對應的 ref 上」,實際是 `_renderEffect`,它比一般 effect 多了與渲染排程對齊的語意(更新會被批次到同一個時機執行,避免一幀之內寫好幾次 DOM)。至於他一句帶過的「處理 directive 與事件的其他 helper」,其中 `_delegate` 值得單獨拿出來:事件不是綁在每個節點上,而是委派到共同的祖先,這是 Vapor 另一項省記憶體的手段。 最後一個容易被忽略的細節:畫面右上角的 Vue Version 顯示的是 `@db140a1`,一個 commit 雜湊而不是版本號——說明這個 playground 是從 vapor 分支建出來的,正好呼應「還在開發中」這件事。
術語:patch flagrenderEffectevent delegationtemplate cloning
7. 整合計畫、限制與結語 3:14–4:08
作者收尾談落地方式。他欣賞 Vue 團隊從 Vue 2 到 Vue 3 的轉換學到教訓:Vapor 不會影響任何既有 app,完全是 opt-in,而且以元件為單位、用類似 .vapor 的寫法啟用,因此同一個 app 裡可以自由混用 Vapor 元件與現在的元件。假設整個 app 都是 Vapor 元件,virtual DOM runtime 就會從 bundle 裡被移除,讓瀏覽器要下載與執行的 runtime 程式碼變少。但他也留下未解的問題:Vapor 與非 Vapor 元件之間的功能對等程度未知,目前已知 Vapor 元件只支援 Composition API 與 script setup,其他限制還不清楚。

推理因為上一段展示的兩份輸出差距太大,讓「整包換過去」聞起來像另一次 Vue 2 → Vue 3,所以作者接著把重心放在遷移策略而不是效能數字:不影響既有 app、完全 opt-in、以元件為單位啟用、可自由混用。他的推理是從歷史教訓倒推設計——Vue 2 到 3 最痛的地方是全有全無,所以這次要把粒度細到單一檔案。也正因為先講清楚了混用機制,他才敢接著談那個最大的好處(整包都是 Vapor 元件就能把 vdom runtime 從 bundle 裡移除)——那是一個條件句,得先知道混用有多容易,才知道這個條件有多難達成。最後他把沒有答案的部分誠實列出來,讓整支影片停在「值得放進雷達」而不是「現在就換」。
- C預設不啟用、以元件為單位主動選擇才生效,不影響既有程式碼→ 畫地圖
- A以元件為單位逐步啟用,不是像 Vue 2→3 整包換過去→ 批判類比
- RComposition API/Options API/script setup 的差別→ 存+回想
- P用 playground 對自己專案的熱點元件試切 Vapor→ 練習
AI 補充這一段的截圖有點反諷:抽幀正好落在動畫還只顯示 `MyButton.vue` 的那一格,作者口中那個標記還沒加上去。這反而提醒了一件事——當時那個寫法本來就只是提案中的候選之一,作者自己也用「something like」帶著保留在講。 有一個推論作者略過了,但它決定你現在該怎麼看待 Vapor 的收益:「整個 app 都是 Vapor 元件才能移除 vdom runtime」是個非常硬的條件。只要用到任何一個依賴 virtual DOM 的第三方元件庫、任何一個還沒轉換的舊元件,這個好處就完全消失。所以在可預見的期間內,Vapor 的實際收益主要來自熱點——大型清單、頻繁更新的表格、動畫密集的介面——而不是整體 bundle 縮小。把它當成「針對痛點的局部工具」比當成「全域效能開關」更接近現實。 opt-in 的代價也值得指出來,因為它正是作者那句 feature parity 疑慮的具體來源:兩套執行模型並存,等於 Vue 要長期維護兩條 runtime,而最容易出問題的地方一定是模式交界——Vapor 父元件裡放非 Vapor 子元件、或反過來,slot、provide/inject、transition 這些跨越元件邊界的機制要怎麼在兩種模型之間對接,都是真正的難題。 至於他提到只支援 Composition API 與 script setup,這不是任性的限制,而是模型衝突:Options API 依賴 `this` 指向元件實例、依賴執行期才組裝起來的選項物件,這與「編譯期就把每個動態位置和它的來源綁死」的做法天生不相容。換句話說,這條限制不太可能靠後續開發解除。
「even though it's still in development and not ready to use yet」
「to happen at the component level using something. vapor.」
4. 總結
作者的推導是一條先立承諾、再拆成本、最後還原機制的鏈。他先丟出一個可檢驗的承諾:元件一行都不用改,app 卻更快、更省記憶體——因為執行時不再使用 virtual DOM。但這張欠條要兌現,得先知道 virtual DOM 現在在做什麼,所以他把 Vue 的執行路徑整條攤開:template 在 build time 編譯成 render function,render function 產生 virtual DOM tree,再用真實 DOM 指令掛上頁面;reactive data 一變就重跑整條路徑,新舊兩棵樹 diff 之後 patch 回真實 DOM。接著他先替 Vue 說好話,用 static hoisting 說明「編譯期知道得比 runtime 多」——這句誇獎其實是為了讓反面論證更有力:連做了編譯期最佳化的 virtual DOM 都還有削不掉的固定成本,要佔記憶體、資料一變就要重建一堆 v-node、還要走完整棵樹找差異。成本落點確定後,他用一句反問換掉整個前提:如果資料改變時我們早就知道 DOM 的哪幾個位置會受影響呢?那 diff 就是在重算一個已知的答案,於是不只 render function 不用重跑、樹不用比對,連 virtual DOM tree 本身都可以不要——這就是 Vapor Mode。這個結論押在「早就知道哪裡會變」這個前提上,所以他立刻把前提還原成 Vue 既有的 reactivity system:同一套追蹤機制,只要把訂閱粒度從整個元件的 render function 縮小到單一 DOM 節點,前提就成立,不需要任何新發明。理論齊了他就換成證據模式,用 SFC Playground 把同一份 App.vue 在 VAPOR OFF 與 VAPOR ON 下的編譯輸出並排:一邊是 createElementVNode 組出來的樹,一邊是 n0、n1 這些真實節點變數加上 renderEffect 直接寫文字,前面所有說法在這裡從示意圖變成程式碼。最後他從技術回到工程現實:完全 opt-in、以元件為單位啟用、可與現有元件混用;好處的兌現條件是整包都是 Vapor 元件才能把 vdom runtime 從 bundle 移除;還沒有答案的是功能對等程度與 Composition API 之外的限制。
勘誤總整理
確定錯誤/已過時見仁見智(取決於版本或情境)
| 段落 | 原話(transcript 逐字) | 說明 |
|---|---|---|
| 7. 整合計畫、限制與結語 3:14 |
「even though it's still in development and not ready to use yet」 | 這句在影片發布的 2024 年 4 月成立,今天已經過時。Vapor Mode 後來隨 Vue 3.6 進入正式的版本線,不再只是一個要自己 build 的實驗分支——影片裡那個顯示 commit 雜湊的 playground 也已經換成正式版本號。要看 Vapor 現在的狀態與可用範圍,請以 Vue 官方 3.6 的文件為準,不要照這支影片的結論以為它還不能碰。 依據: Vue 3.6(Vapor Mode 隨核心釋出,2025 年起提供) |
| 3. 編譯期最佳化:static hoisting 0:59 |
「that tools like react don't or at least don't do yet」 | 在 2024 年 4 月成立,但這條界線後來被 React Compiler 模糊掉了:React 也有了編譯步驟,會自動做 memoization 並提升靜態的 JSX 元素。要注意兩者仍不等價——React Compiler 省的是「元件函式重跑與 re-render」,Vue 省的是「virtual node 的建立與比對」,切入的層級不同。所以這句話今天不能再當成「只有 Vue 有編譯期最佳化」來讀,但也不能反過來說 React 已經追平。 依據: React Compiler(2024 年推出 beta/RC,之後隨 React 19 生態普及) |
| 7. 整合計畫、限制與結語 3:30 |
「to happen at the component level using something. vapor.」 | 作者自己用「something like」保留過,而最後定案的啟用方式與這裡描述的檔名慣例不同:實際是在單檔元件的 script 區塊上加一個 vapor 標記(`<script setup vapor>`),而不是把檔案改名成 .vapor.vue。「以元件為單位、可與現有元件混用」這個大方向沒有變,變的只是寫法。看這段時請把重點放在粒度,不要把那個檔名當成 API。 依據: Vue 3.6 的 vapor 啟用方式(script 區塊屬性) |
5. 推薦三個下一步
1. 往下挖深:Vue reactivity 的追蹤機制
整支影片的結論押在「資料變時早就知道哪個 DOM 節點受影響」這個前提上,而作者只用一句 reactivity system 帶過。要真正相信這條推論,得看懂 Proxy 怎麼在讀取時 track、寫入時 trigger,以及 effect 的訂閱粒度為什麼可以縮到單一節點。
YouTube 搜尋:Vue 3 reactivity explained proxy track trigger how does Vue reactivity work under the hood signals vs virtual dom fine-grained reactivity
2. 往旁邊對照:其他無 virtual DOM 的框架
作者說 Vapor 的靈感來自 Solid JS,但沒說 Solid 怎麼做、也沒比較 Svelte 這條同樣走編譯路線卻做法不同的路。看過對照組,才分得出哪些是 Vapor 的設計選擇、哪些是這一整類方案的共同代價。
YouTube 搜尋:SolidJS fine-grained reactivity no virtual DOM Svelte compiler vs virtual DOM Solid vs Svelte vs Vue Vapor comparison
3. 往上應用:實際跑一次 Vapor 並量效能
影片只給了 Playground 的編譯輸出,沒有任何實測數字,「更快、更省記憶體」到目前為止仍是承諾。自己開一個 Vapor 專案、用 DevTools 的 Performance 與 Memory 面板量 bundle 大小與更新耗時,才知道這筆好處在自己的場景值多少。
YouTube 搜尋:Vue Vapor Mode tutorial setup Vue 3.6 vapor benchmark performance Chrome DevTools memory profiling JavaScript framework
- 接著看 ←Reactivity in Vue 3 - How does it work? · Vapor 拿掉 vdom 的本錢就是 effect / track / trigger,那站先把它拆到底
- 相關 —15 Frontend Concepts Every Senior Dev Has Mastered · 那站建立瀏覽器渲染路徑與 virtual DOM 的成本觀,這站主張把中間那層拿掉
- 相關 —Every Frontend Architecture Pattern Explained in 23 Minutes · 架構演化史停在 vdom 時代,Vapor 是編譯派給出的下一站
- 接著看 →Frontend System Design: Performance API, Chrome, React Profiler · Vapor 只給了「更快更省」的承諾沒給數字,這站教你怎麼量出來
- 相關 —Senior Frontend Interview 2026 🎉 | Javascript 🎯 (Mock) [Most Asked Questions] · 同談 virtual DOM 與 DOM 操作成本,一個講原理一個在面試裡被問
- 相關 —9 JavaScript Concepts That Got Me To Senior Dev · 都在講「同步重算」與「只改該改的」之間的成本差,一個在語言層一個在框架層