一、前言
網上都說操作真實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