【vue系列】Virtual DOM 真的比操作原生 DOM 快嗎?

saucxs發表於2019-06-27

一、前言

網上都說操作真實dom怎麼怎麼慢,這兒有個例子:http://chrisharrington.github.io/demos/performance/,例子迴圈2000個隨機陣列,點選按鈕重新生成隨機陣列渲染頁面,也是自己用的js 操作dom 比用react 和angular 都要快。這是引用知乎上的一個問題。覺得寫的很好。文章內容主要是來源於尤大大的回答。

 

二、原生dom操作VS通過框架封裝操作

這是一個效能 vs. 可維護性的取捨。框架的意義在於為你掩蓋底層的 DOM 操作,讓你用更宣告式的方式來描述你的目的,從而讓你的程式碼更容易維護。沒有任何框架可以比純手動的優化 DOM 操作更快,因為框架的 DOM 操作層需要應對任何上層 API 可能產生的操作,它的實現必須是普適的。針對任何一個 benchmark,我都可以寫出比任何框架更快的手動優化,但是那有什麼意義呢?在構建一個實際應用的時候,你難道為每一個地方都去做手動優化嗎?出於可維護性的考慮,這顯然不可能。框架給你的保證是,你在不需要手動優化的情況下,我依然可以給你提供過得去的效能。

 

三、 對 React 的 Virtual DOM 的誤解

React 從來沒有說過 “React 比原生操作 DOM 快”。React 的基本思維模式是每次有變動就整個重新渲染整個應用。如果沒有 Virtual DOM,簡單來想就是直接重置 innerHTML。很多人都沒有意識到,在一個大型列表所有資料都變了的情況下,重置 innerHTML 其實是一個還算合理的操作... 真正的問題是在 “全部重新渲染” 的思維模式下,即使只有一行資料變了,它也需要重置整個 innerHTML,這時候顯然就有大量的浪費。

我們可以比較一下 innerHTML vs. Virtual DOM 的重繪效能消耗:

(1)innerHTML: render html string O(template size) + 重新建立所有 DOM 元素 O(DOM size)

(2)Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)

Virtual DOM render + diff 顯然比渲染 html 字串要慢,但是!它依然是純 js 層面的計算,比起後面的 DOM 操作來說,依然便宜了太多。可以看到,innerHTML 的總計算量不管是 js 計算還是 DOM 操作都是和整個介面的大小相關,但 Virtual DOM 的計算量裡面,只有 js 計算和介面大小相關,DOM 操作是和資料的變動量相關的。前面說了,和 DOM 操作比起來,js 計算是極其便宜的。這才是為什麼要有 Virtual DOM:它保證了 1)不管你的資料變化多少,每次重繪的效能都可以接受;2) 你依然可以用類似 innerHTML 的思路去寫你的應用。

 

四、 MVVM vs. Virtual DOM

相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 採用的都是資料繫結:通過 Directive/Binding 物件,觀察資料變化並保留對實際 DOM 元素的引用,當有資料變化時進行對應的操作。MVVM 的變化檢查是資料層面的,而 React 的檢查是 DOM 結構層面的。MVVM 的效能也根據變動檢測的實現原理有所不同:Angular 的髒檢查使得任何變動都有固定的
O(watcher count) 的代價;Knockout/Vue/Avalon 都採用了依賴收集,在 js 和 DOM 層面都是 O(change):

(1)髒檢查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)

(2)依賴收集:重新收集依賴 O(data change) + 必要 DOM 更新 O(DOM change)可以看到,Angular 最不效率的地方在於任何小變動都有的和 watcher 數量相關的效能代價。但是!當所有資料都變了的時候,Angular 其實並不吃虧。依賴收集在初始化和資料變化的時候都需要重新收集依賴,這個代價在小量更新的時候幾乎可以忽略,但在資料量龐大的時候也會產生一定的消耗。

MVVM 渲染列表的時候,由於每一行都有自己的資料作用域,所以通常都是每一行有一個對應的 ViewModel 例項,或者是一個稍微輕量一些的利用原型繼承的 "scope" 物件,但也有一定的代價。所以,MVVM 列表渲染的初始化幾乎一定比 React 慢,因為建立 ViewModel / scope 例項比起 Virtual DOM 來說要昂貴很多。這裡所有 MVVM 實現的一個共同問題就是在列表渲染的資料來源變動時,尤其是當資料是全新的物件時,如何有效地複用已經建立的 ViewModel 例項和 DOM 元素。假如沒有任何複用方面的優化,由於資料是 “全新” 的,MVVM 實際上需要銷燬之前的所有例項,重新建立所有例項,最後再進行一次渲染!這就是為什麼題目裡連結的 angular/knockout 實現都相對比較慢。相比之下,React 的變動檢查由於是 DOM 結構層面的,即使是全新的資料,只要最後渲染結果沒變,那麼就不需要做無用功。

Angular 和 Vue 都提供了列表重繪的優化機制,也就是 “提示” 框架如何有效地複用例項和 DOM 元素。比如資料庫裡的同一個物件,在兩次前端 API 呼叫裡面會成為不同的物件,但是它們依然有一樣的 uid。這時候你就可以提示 track by uid 來讓 Angular 知道,這兩個物件其實是同一份資料。那麼原來這份資料對應的例項和 DOM 元素都可以複用,只需要更新變動了的部分。或者,你也可以直接 track by $index 來進行 “原地複用”:直接根據在陣列裡的位置進行復用。在題目給出的例子裡,如果 angular 實現加上 track by $index 的話,後續重繪是不會比 React 慢多少的。甚至在 dbmonster 測試中,Angular 和 Vue 用了 track by $index 以後都比 React 快: dbmon (注意 Angular 預設版本無優化,優化過的在下面)

順道說一句,React 渲染列表的時候也需要提供 key 這個特殊 prop,本質上和 track-by 是一回事。

 

五、效能比較也要看場合

在比較效能的時候,要分清楚初始渲染、小量資料更新、大量資料更新這些不同的場合。Virtual DOM、髒檢查 MVVM、資料收集 MVVM 在不同場合各有不同的表現和不同的優化需求。Virtual DOM 為了提升小量資料更新時的效能,也需要針對性的優化,比如 shouldComponentUpdate 或是 immutable data。

(1)初始渲染:Virtual DOM > 髒檢查 >= 依賴收集

(2)小量資料更新:依賴收集 >> Virtual DOM + 優化 > 髒檢查(無法優化) > Virtual DOM 無優化

(3)大量資料更新:髒檢查 + 優化 >= 依賴收集 + 優化 > Virtual DOM(無法/無需優化)>> MVVM 無優化

不要天真地以為 Virtual DOM 就是快,diff 不是免費的,batching 麼 MVVM 也能做,而且最終 patch 的時候還不是要用原生 API。在我看來 Virtual DOM 真正的價值從來都不是效能,而是它

 (1) 為函式式的 UI 程式設計方式開啟了大門;(2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。

注:react本身遵循的就是 UI = fn(state) 這樣的一個公式,這裡的fn 就是函式,通過state去觸發fn(在這個過程是有很多複雜的計算操作,比如Virtual DOM對比),最後導致UI的更新,不知道我理解的對不對。

 

六、總結

以上這些比較,更多的是對於框架開發研究者提供一些參考。主流的框架 + 合理的優化,足以應對絕大部分應用的效能需求。如果是對效能有極致需求的特殊情況,其實應該犧牲一些可維護性採取手動優化:比如 Atom 編輯器在檔案渲染的實現上放棄了 React 而採用了自己實現的 tile-based rendering;又比如在移動端需要 DOM-pooling 的虛擬滾動,不需要考慮順序變化,可以繞過框架的內建實現自己搞一個。

 

七、參考

尤大大的回覆:https://www.zhihu.com/question/31809713/answer/53544875

 

相關文章