網上都說操作真實 DOM 慢,但測試結果卻比 React 更快,為什麼?

網上都說操作真實dom怎麼怎麼慢,但是下面這個鏈接案例中原生的方式卻是最快的

http://chrisharrington.github.io/demos/performance/

我在本地也寫了個例子循環2000個隨機數組,點擊按鈕重新生成隨機數組渲染頁面,也是自己用的js 操作dom 比用react 和angular 都要快,這個是怎麼回事。測試方式不對,頁面元素太少了,還是哪裡問題,請幫忙解答下,謝謝。


這裡面有好幾個方面的問題。

1. 原生 DOM 操作 vs. 通過框架封裝操作。

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

2. 對 React 的 Virtual DOM 的誤解。

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

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

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

  • 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 的思路去寫你的應用。

3. 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)

  • 臟檢查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)
  • 依賴收集:重新收集依賴 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 是一回事。

4. 性能比較也要看場合

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

  • 初始渲染:Virtual DOM &> 臟檢查 &>= 依賴收集
  • 小量數據更新:依賴收集 &>&> Virtual DOM + 優化 &> 臟檢查(無法優化) &> Virtual DOM 無優化
  • 大量數據更新:臟檢查 + 優化 &>= 依賴收集 + 優化 &> Virtual DOM(無法/無需優化)&>&> MVVM 無優化

不要天真地以為 Virtual DOM 就是快,diff 不是免費的,batching 么 MVVM 也能做,而且最終 patch 的時候還不是要用原生 API。在我看來 Virtual DOM 真正的價值從來都不是性能,而是它 1) 為函數式的 UI 編程方式打開了大門;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。

5. 總結

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


答案很簡單,因為網上說的是錯的。

React.js 的演算法本質上是重刷視圖,相當於設置 innerHTML,只不過它設置的是內存裡面 Virtual-DOM,而不是真實的 DOM。你要是拿它和真實的 innerHTML 比那肯定是 React.js 更快。

但實際上我們並不會這麼成片成片地通過 innerHTML 去更新視圖(你要是用 Backbone.js 我無話可說),而更多的是精細化地一個數據對應一個 DOM 去更新(例如用jQuery)。假設數據變化對應的手動 DOM 操作都是儘可能一一對應的(最小化 DOM 操作),那麼 React.js 對比我們手動操作 DOM 來說毫無性能可言,因為在得到最小化的 DOM 操作結果之前你還有一個 diff 演算法。

React.js 厲害的地方並不是說它比 DOM 快(這句話本來就是錯的),而是說不管你數據怎麼變化,我都可以以最小的代價來更新 DOM。方法就是我在內存裡面用新的數據刷新一個虛擬的 DOM 樹,然後新舊 DOM 樹進行比較,找出差異,再更新到真正的 DOM 樹上。

這就是所謂的 diff 演算法,雖然說 diff 演算法號稱演算法複雜度 O(n) 可以得到最小操作結果,但實際上 DOM 樹很大的時候,遍歷兩棵樹進行各種對比還是有性能損耗的,特別是我在頂層 setState 一個簡單的數據,你就要整棵樹 walk 一遍,而真實中我可以一句 jQuery 就搞定,所以就有了 `shouldComponentUpdate` 這種東西。

而且不管 Vue 還是 React.js 它們的列表對比演算法都不是最優演算法,最優演算法是 Levenshtein distance 動態規劃,演算法複雜度 O(n^2),React.js 接受不了,就搞了一個簡單的 O(n) 版本的。所以在某些情況下即使帶了 `key`,也可能帶上了不必要的元素移動操作(對比最優演算法而言),所以它號稱最小的 DOM 操作其實也是有點瑕疵的。

對比人肉版本的手工最小 DOM 操作,React.js 無疑是輸家,diff 性能有損耗,也不是所有情況都是最小 DOM 操作。但是勝在方便,一切都是 trade off。

所以說,同學們,不要見的風就是雨啊,老想搞個大新聞。


我最近搞虛擬DOM,就說說avalon 是怎麼更新數據的吧,react的實現方式也差不多

&
&
&
&
&

注意到列表多了許多注釋節點吧,這叫做路標系統,用於確定數組元素的作用域範圍,方便移動它們。大家如果會angular,使用ng-repeat也會有這樣的注釋元素。但這些不是重點。

為了讓大家明白虛擬DOM為什麼這麼高效的原因,我們直接在chrome控制台下對這些LI元素做一些修改,全部加上title屬性。它們能方便告訴我們,一會兒點擊了change按鈕,重新渲染列表,這些LI元素是新建的,還是沿用的舊的。

當我們點擊change按鈕後

我們可以發現,能重複利用的節點被重複利用,它是優先考慮移動節點,這時它不用一一改動LI元素裡面的所有要改動的地方;對於新添加的元素,它會利用上次被刪除的元素對應的節點,然後改變其屬性值 ;當沒有可利用的節點,框架才會將虛擬節點轉換真實節點。在某些MVVM框架,需要用戶顯式寫「track by $index」, react則要求用key,avalon是內部使用hash演算法搞定。

這只是diff演算法的冰山一角。

其實,如果沒有循環綁定(ms-repeat, ng-repeat, v-for),MVVM是不存在性能問題

循環綁定會破壞原來的頁面結構,引起頁面重新布局。

傳統的字元串模板,對付它們只會innerHTML,這導致每次都會生成大量的節點,選區,游標位置,事件及掛在它上面的第三方組件都要重新處理。

而一些弱的MVVM框架,它們的演算法就差一點,無法盡量利用已有節點。

avalon與react的做法是使用了虛擬DOM做緩衝層,每次數據變動,都生成對應區域的虛擬DOM樹,然後兩個虛擬DOM做合併操作(diff),得到更新的操作集(patch)。

avalon目前沒有考慮支持多種渲染形態(只有DOM,不存在canvas, webgl, native等渲染介質),那麼可以偷懶,對不存在綁定的元素及其孩子,不再轉換為虛擬DOM,這樣減少diff的節點。

創建一個DOM的消耗是非常驚人,目前,在瀏覽器中存在四種形態的對象

  1. 超輕量 Object.create(nulll)

  2. 輕量 一般的對象 {}

  3. 重量 帶有訪問器屬性的對象, avalon或vue的VM對象

  4. 超重量 各種節點或window對象

然後頻繁訪問或創建 3,4種形態的對象是很不明智的。但VM對象能為編程帶來極好的用戶體驗,因此這個代價是可以接受的。

在過去的MVVM框架中,綁定屬性的操作主體是元素節點(它們一般放在觀察者模式的訂閱數組中),有了虛擬DOM,我們就直接操作虛擬DOM,這樣它們移出或插入虛擬DOM樹,是不耗費什麼性能,更不會結合CSS樣式表產生reflow,rerender 什麼破事。 因此原真實DOM擁有了綁定屬性(ms-*, ng-*)與路標系統 ,以後我們將虛擬DOM樹的變更同步到真實DOM樹時是非常方便的。

而react是怎麼追蹤到其更變更的節點呢?它有data-reactid!只要確保對應關係,虛擬DOM怎麼插入,移除,重排,react會在一個事務的盡頭,才開始重新真實DOM,這樣也能確保更新成功。

總而言之,有了虛擬DOM,我們是使用夠輕量的對象代替超重對象作為直接操作 主體,減少對超重對象的操作!虛擬DOM的結構是很輕量,最多不超過10個屬性,並且其繼承層級不超過2層。而DOM節點有70+個屬性,繼承層級有6,7層(文本節點6層,元素節點7層).訪問一個屬性,可能會追溯幾重原型鏈

通過JS修改屬性時觸發DOM回調 · Issue #272 · RubyLouvre/avalon · GitHub

其二,能在虛擬DOM與真實DOM建立映射關係.avalon是使用綁定屬性與路標系統 ,react使用data-reactid,但data-reactid是元素節點的自定義屬性,對於文本節點,它沒有辦法,只好外包一個span,因此嚴重破壞舊有結構。即便插入了這麼span,react性能還是很快。

第三,react與avalon的所有虛擬DOM對象是可回收循環利用。而節點循環利用是很危險,很易造成內存泄漏。

虛擬DOM不只是一個緩衝層,裡面涉及大量演算法,你可以使用hash或KMP,確保更新最少。或者在某級對象創建一個更新對象包,將重複變更的屬性放在裡面,這樣aaa=bbb, aaa=ccc,aaa=ddd就自動合併成一個aaa=ddd。

為了追求性能,react強制單向流動,方便讓狀態疊加。但雙向綁定也能疊加,只是會讓VM內部數據多轉幾圈。

無論怎麼樣,虛擬DOM是一個偉大的發明,但react實現得比較笨拙而已。隨著大家對其源碼的研讀,今年會冒出更多同類型產品。前端就是因為源碼公開而繁榮昌盛!因此大家必須讀源碼,只會調API,永遠是低級碼農。


各按10次,結果如下


鑒於有人表示沒有看懂。 下面是結論:

  • 題主測試結果反常是因為測試用例構造有問題。
  • React.js 相對於直接操作原生DOM有很大的性能優勢, 背後的技術支撐是基於virtual DOM的batching 和diff.

  • React.js 的使用的virtual DOM 在Javascript Engine中也有執行效率的優勢。

如果想知道解釋,請繼續閱讀。。

-----------------------------------------

看了好幾個答案感覺都沒有答到點子上面。如果一個框架發明出來就是為了提高性能,那就一定要在性能上面有所體現。編程模型類型框架實在是太多太多,React.js僅僅因為這個不可能會那麼受好評。

React.js僅僅是做MVC中的View,也就是渲染層,任務其實非常單一的,也沒有那麼多哲學方面的東西需要承載。

1.題主測試結果反常是因為測試用例構造有問題。

我們來看看題主的測試用例(由於題主問的react.js 和原生,所以其他的我都去掉了).

以下所有的代碼都在:

http://fredrikluo.github.io/testcases/reactjstest.html

原生 (只保留了關鍵代碼,其他都去掉了,這樣看得更清楚一點)

這裡看出問題沒?

整個測試用例的模式是:

  1. 構造一個 String, 包含一個1000個 Tag。

  2. 把這1000個 Tag 一次性添加到 DOM 樹中。

其實,題主的測試用例只做了一次 DOM 操作。。而且主要問題是,如果你真的做一個WebAPP, 然後直接操作DOM更新,更新的模式完全不是這樣子的。

在現實中,更新模式更像這個樣子滴:

同樣是 1000 元素需要更新, 你的界面上面分成了20個邏輯區域(或者層,或者 view, 或者whatever 框架取的名字), 每個區域 50 個元素。在界面需要更新的時候,每個邏輯區域分別操作 DOM Tree 更新

那麼代碼看起來更像是這樣子的:

然後我們再來看看結果

天了擼, 發生了什麼, 原生的怎麼慢這麼多。(React.js 並不需要修改,無論如何每個區域都是把新的操作作用在Virtual DOM上面,然後每幀只會調用一次Render)。

2. React.js 相對於直接操作原生DOM有很大的性能優勢, 背後的技術支撐是基於virtual DOM的batching 和diff。

原生DOM 操作慢嗎?

做為一個瀏覽器內核開發人員, 我可以負責任的告訴你,慢。

你覺得只是改變一個字元串嗎?

我們來看看你在插入一個DOM元素的時候,發生了什麼.

實際上,瀏覽器在收到更新的DOM Tree需要立即調用HTML Parser對傳入的字元串進行解析,這個,呃,耗的時間可不是字元串替換可以比的哦 .

(其實你們已經處於一個好時代了,換做幾年前,瀏覽器還可能會花幾秒到幾分鐘給你Reflow一下)

這個例子還算簡單的了,如果你插入的標籤裡面包括了腳本,瀏覽器可能還需要即時編譯的你腳本(JIT).

這些時間都算在你的DOM操作中的哦

我們再來看看統計。

131 ms 都花在了Loading 裡面(ParseHTML)

另外注意一些細節,Profiler 報告整個函數使用了418ms, 因為有些時間在JS裡面是統計不到的,比如Rendering的時間, 所以, 多用Profiler.

我們再來看一個圖, 這個原測試用例的Profiling, 1000 Tag 一次插入

呃,如果你還沒有看出端倪的話,我提示一下: 這裡的解析時間(Loading)降到了13 ms.

同樣的數據(1000 元素),分20次解析, 每次解析50個,耗時是 一次性解析的 10 倍左右。。也就是說,有9倍開銷,都花在了DOM函數,以及背後的依賴的函數本身的開銷上了。 DOM 操作本身可能不耗時,但是建立上下文,層層傳遞的檢查的開銷就不容小視了

這個官方稱為」API Overhead」. 改變DOM 結構的調用都有極其高的API Overhead.

而對於Overhead高的API,標準解決辦法就是兩個:

Batching 和 Diff.

Diff 大家都比較了解, Batching是個啥?

想像在一個小山村裡面,只有一條泥濘的公路通向市區,公交車班次少,每次要開半個小時,如何保證所有乘客最快到達目的地?

搜集盡量多的乘客,然後一次性的把它們運往市區, 這個就是Batching.

如果你搜下React.js 的文檔裡面。這裡有專門提到:

「 You may be thinking that it"s expensive to change data if there are a large number of nodes under an owner. The good news is that JavaScript is fast and `render()` methods tend to be quite simple, so in most applications this is extremely fast. Additionally, the bottleneck is almost always the DOM mutation and not JS execution. React will optimize this for you using batching and change detection.「

這個才是React.js 引入Virtual DOM 的精髓之一, 把所有的DOM操作搜集起來,一次性提交給真實的DOM.

Batching 或者 Diff, 說到底,都是為了盡量減少對慢速DOM的調用。

類似技術在遊戲引擎裡面(對 OPENGL 的Batch), 網路傳輸方面( 報文的Batch), 文件系統讀寫上都大量有使用。

3. React.js 的使用的virtual DOM在Javascript Engine中也有執行效率的優勢

Virtual DOM 和 DOM 在JS中的執行效率比較

-------------------------------------------

前方高能預警, 一般前端開發不需要了解那麼多,不過如果你如果都看懂了,來歐朋瀏覽器面試吧 :)

拋開瀏覽器內核開銷不算, 從Javascript 執行效率來講,Virtual DOM 和DOM之間到底有多大差別呢?

這個其實可以回答Virtual DOM 到底有多快這個問題上面。 了解這個問題,需要了解瀏覽器內核,DOM以及網頁文檔到底都是什麼關係。

很吃驚吧。

其實DOM 完全不屬於Javascript (也不在Javascript 引擎中存在). Javascript 其實是一個非常獨立的引擎,DOM其實是瀏覽器引出的一組讓Javascript操作HTML文檔的API而已。

如果你了解Java的話,這個相當於JNI.

那麼問題就來了, DOM的操作對於Javascript實際上是外部函數調用。 每次Javascript 調用DOM時候,都需要保存當前的上下文,然後調用DOM, 然後再恢復上下文。 這個在沒有即時編譯(JIT)的時代可能還好點。 自從加入即時編譯(JIT), 這種上下文開銷簡直就是。。慘不忍睹。

我們來順便寫一段Javascript.

然後在v8裡面跑一跑(直接使用v8 sample中的shell ). 由於v8 單獨的Shell中不存在DOM, 我們用print代替, print 是外部函數, 調用它和調用DOM是一回事。

調用的堆棧看起來是這樣的。

這裡可以看到V8是如何執行JIT代碼,然後JIT代碼調用到Print的過程 (JIT 代碼就是沒有符號的那一堆,Frame #1 - #5.)

我們來看看v8 JIT 生成的代碼.

看到這裡還算合理, 一個call調走,不過我們來看看 CallApiAccessorStub是個什麼鬼:

60+ 條額外指令用於保存上下文和堆棧檢查。。

我靠,我就調個函數,你至於嗎..

當然,現代的JIT技術已經進步很多了,換到幾年前,這個函數直接就不JIT了 (不編譯了, 在V8中即不優化,你懂的,慢10到100倍).

而Virtual DOM的執行完全都在Javascript 引擎中,完全不會有這個開銷。

什麼,你說我第一個測試結果裡面兩邊的速度就差一半嘛(117 ms vs 235 ms),react.js還只是做了一次DOM操作,原生的可是做了50次哦, 你說的virtual DOM框架完全不會有開銷是幾個意思?

我們來稍微改改測試代碼。

給你們每個條目加個標示符,這樣每次更新的DOM都不一樣,我看React.js你怎麼做Diff, 哇哈哈哈哈哈.

然後你可以多點幾次React的測試按鈕,一般來說來,第二次以後, 你就可以看到性能穩定在這個數字。

這個是個什麼情況? 這個可不是React.js Diff的功勞哦?因為我們每次的更新都是完全不同的,它木有辦法佔便宜做Diff哦。。

這個其實是Javascript 引擎的工作特性引起。Javascript 引擎目前用的都是即時編譯(JIT). 所以

  • 第一次點擊運行的時候所耗的時間 = 框架被編譯的時間(JIT) + 執行時間

  • 之後執行的時間 = 執行時間。

所以, 53 ms那個才是Virtual DOM 的真實執行性能哦, 是不是覺得好快呀

當然, v8的JIT方法還要特殊一些, 他是兩次編譯的, 第一次粗編譯,第二次會把執行很多的函數進行精細編譯. 所以第三次以後才是最快的時間。

兩次編譯雖然很有趣也很有用,鑒於這個帖子實在是太長了,這裡就不講了。 有興趣看這個:

v8: a tale of two compilers -- wingolog

原測試用例在測react第二次運行的時候會很慢(大概4s左右), 原因是這個:

onClick: this.select.bind(null, this.props.data[i])

bind 會每次創建一個新的函數對象,於是原測試裡面每次點擊會創建1000個新的函數對象。恭喜原作者,JS的內存真是不要錢。。

我的測試用例裡面暫時去掉了,徹底修復可以不用bind, 指向一個函數即可,然後用其他方法為每個列表項保存狀態。

-------------------------------------------------------------------------------------------------------------

回答 @楊偉賢

你用的技巧稱為是 DocumentFragment, 參見

Document Object Model (Core) Level 1

這個改善性能技巧早期流行其實主要是防止瀏覽器在JS修改DOM的時候立即Reflow。(防止你在修改了DOM以後立即取元素大小一類的)。

這個技巧在現代瀏覽器裡面基本沒有作用,因為,基本上Reflow和Layout都是能延遲就延遲。

不相信的話你這樣寫

var tplDom = document.createElement("div");
container.appendChild(tplDom);
tplDom.innerHTML = html;
html = "";

tplDom先加入global DOM然後在修改,這個算是刷global DOM了吧?

在chromium裡面沒有任何區別的。

我更改以後的測試用例裡面就是這樣寫的:)

所以,這個和刷不刷global DOM沒有任何關係的。你看到的性能提升,其實是避免了

container.innerHTML += html;

中+=, 因為+= 是其實是一個替代操作,而不是增量操作。

另外, 你測出來的只是插入新元素時間而代碼裡面還需要刪掉之前的節點的代碼.

因為對於react.js

第一次運行時間 = JIT時間+插入時間

多次以後 = 更新節點時間。 (v8優化器原因)

而更新節點 = 刪除節點+插入新的節點 (你也可以用replaceChild, 性能沒有能測出來差異).

react.js由於第一次運行帶著JIT,所以沒有辦法剝離出來純插入時間。於是加入刪除之前節點的代碼,然後多次運行測試更新節點的時間才是有比較性的。

修改後的測試用例在

http://fredrikluo.github.io/testcases/reactjstest-1.html

你可以試試,這個是我這裡測出來的結果。

很接近了是不是?不可否認這是一個非常好的優化,

是不是覺得React已經沒有什麼優勢?

少年,你太天真了,瀏覽器這個世界很險惡的,我剛才說Layout和Reflow被怎麼了來著?

被延遲了。。

我們來看看profiler的輸出。

React 的性能統計

Raw的性能統計

事實上React的耗用的時間的是80m, 而優化後的Raw也有144 ms, 接近一半的時間。

為蜀莫呢?

有個31ms 和95ms的rendering差別,而在這個例子裡面, rendering = Recalculate Style 和Layout(參見上面的堆棧,或者你自己也可以試試)。而這個差異,在JS裡面是測不到了,因為, 呃,他們被延遲了。。

原因也很簡單,React 修改的粒度更小。

virtual DOM每次用Batch + diff來修改DOM的時候, 假如你有個& abc&如果只是內容變了,那spanNode.textContent會被換掉, style啥的不會動。 而在原生例子裡面,整個spanNode被換掉。

瀏覽器: 你把整個節點都換了,我肯定只能重新計算style和layout了, 怪我咯。

從本質上面講, react.js 和自己寫演算法直接操作原生的DOM是這樣一個關係:

修改的內容 -&> React.js -&> DOM操作

修改的內容 -&> 你的演算法 -&> DOM操作

React.js 和你的演算法最後都是把特定的意圖翻譯成一組DOM操作,差別只是誰的翻譯更加優化而已

而原回答其實想說明的時候,react.js 在使用virtual DOM這套演算法以後DOM操作在通常情況下比自己的寫是優化非常多的。這個其實是對「使用react.js和(用自己演算法)操作DOM哪個快」這個問題的直接回答。 而你當然可以進一步優化演算法,在特定的環境下面接近或者超過React, 不過,這個在實際開發中並沒有適普性。

這個其實和問編譯器和手動彙編哪個快是一模一樣

演算法 -&> 原代碼 -&>編譯器-&>彙編

演算法 -&>手動翻譯-&>彙編

而在目前CPU的複雜程度下,手動翻譯反而大部分時間比不上編譯器, 複雜度越高,需要考慮的變數越多,越容易用演算法來實現而人腦總是愛忘東西的。

不可否認是原測試用例裡面的不管是測raw還是測react的的演算法都寫得很有問題。所以對原回答中只是盡量不做大的修改來說明問題而已。

最後還要感謝你的提問,否則沒法講得這個深度,其實在寫virtual DOM JIT那一段的時候我就很猶豫是不是講得有點過了。

-----------------------------------------------------------------------------------------------------------

回答 @尤雨溪

呃。。你說的這句話是吧 - 「發明出來就是為了提高性能」 以及 「沒有那麼多哲學方面的東西需要承載」。

既然我們討論的是「發明出來」, 我們就來看看原團隊的意思咯, 戳這裡, 這個原團隊的blog post:

Why did we build React?

-- 摘要如下:

Why did we build React?

There are a lot of JavaScript MVC frameworks out there. Why did we build React and why would you want to use it?

React isn"t an MVC framework.

....

React doesn"t use templates.

...

Reactive updates are dead simple

React really shines when your data changes over time.

....

Because this re-render is so fast (around 1ms for TodoMVC), the developer doesn"t need to explicitly specify data bindings. We"ve found this approach makes it easier to build apps

....

這個才是全文重點, 請再次默念下「really shines

這篇Post翻譯成大白話就是:

哥做了一個框架, 有這個功能,還有這個功能,但是最牛逼的功能就是:你的數據變的時候渲染的特別快哦, 快得你們愛怎麼搞怎麼搞。

真的不是性能嘛?...

PS: 雖然我覺得你說得很有道理,他們都是react.js的好處啦,不過就從"why"來說

  • 但是「通過引入函數式思維來加強對狀態的管理」, 貌似木有在「why」裡面提到?

  • 第三方那個我木有找到引用。
  • 而「Virtual DOM 可以渲染到非 DOM 的後端從而催生 ReactNative」, 是在:「Because React has its own lightweight representation of the document, we can do some pretty cool things with it:」

聽起來貌似是:

「React really shines when your data changes over time.

In a traditional JavaScript application, you need to look at what data changed and imperatively make changes to the DOM to keep it up-to-date. Even AngularJS, which provides a declarative interface via directives and data binding requires a linking function to manually update DOM nodes.

React takes a different approach.」

的副產品哦(by-product).


對於使用原生DOM介面操作大片DOM而言:

  1. 如果對DOM創建過程稍作優化,首次創建必定比所有框架或者庫都快。

  2. 如果對DOM變更過程做相對複雜的優化,它會比大多數框架或者庫都快。

  3. 如果想要DOM的變更過程更快,需要作更加複雜的優化。

很多時候大家討論某些東西的快慢,是基於完全不優化的方式的,這時候得出一個原生操作更慢的結論也不奇怪。實際上,對於每個框架或者庫,對於其中的不同DOM操作類型,基本都存在針對性的優化方式,可以大幅提升性能。


各按兩次或以上,比較最後一次的時間。

詳細原理可以在知乎里查 "虛擬DOM": 怎麼更好的理解虛擬DOM? - React.js 。

大致原理是,第一次渲染,大家都是往空的div里插入新的元素,react還多了個創建虛擬DOM的過程,自然較慢。

但第二次往後,raw的一般思路是刪了原來的元素重新創建元素插入。而react是在虛擬DOM上做比較,對實際的DOM只修改有變動的部分。就這個例子來說,修改原有元素的文本就行了,不需要走 "刪除-創建-插入" 的步驟。

當然,瀏覽器渲染最終還是靠DOM,你有針對性的去優化操作DOM的邏輯(比如說在這個例子里你也在第二次渲染時只修改文本而非刪除後再插入),最終肯定能比react快,只不過代碼寫起來很費時且很難看而已。


Google一名員工的測試結果 https://aerotwist.com/blog/react-plus-performance-equals-what/?utm_source=javascriptweeklyutm_medium=email。

測試中顯示了使用直接操作 DOM (Vanilla 方式)的方式要比React快三倍。測試用例是一個 Flickr 照片流的應用。操作 DOM 的地方就是用戶每點擊一次 "Add Images" 按鈕,就添加 100 張圖片(大約500個 DOM Elements)。

其實這個例子僅僅說明了,在插入「只讀」的 DOM Elements 的場景下,Vanilla 方式要比 React 快。這個例子得到這樣的結果是正常的。

這些插入的 DOM Element 是只讀的,那麼任何框架都只可能是比 Vanilla 方式慢,畢竟要添加抽象。React 的性能優勢在 Diff,而這個場景根本沒 Diff 啥事。

React 幫助在複雜的界面環境下,如何盡量降低 DOM Reflow 的成本。稍微複雜一點的 WebApp ,對 DOM 的操作是非常綜合的,CRUD都有。而一旦出現了頻繁變化,DOM Reflow 的開銷就變得非常大。React 的 Diff 就是解決這個最痛的問題的。

更何況當 WebApp 複雜到一定程度之後,不依賴 Angular、Ember 或者 React 這樣的框架,你的代碼就會完全失控,更何談性能調優。原作者也提到了 React 在工效方面(ergonomics)的優秀性。

另外,大家用 React 的時候如果發現性能問題,先注意是不是用了 minified 的版本。文章的作者一開始也沒注意這一點。生產版的 React 比 Debug 版的要快不少。


一般隨機數據的情況下,近似可以認為DOM變更在50%左右,倘若基數又大,那麼直接innerHTML進行一次性重繪的時間記作t1,vd比對的時間記作t2,vd更新真實DOM的時間記作t3。

顯然2者對比是t1和t2+t3的對比,在這種情況下由於t2的時間消耗導致t1反過來可能比t2+t3的時間要少。

這個問題和測試湊巧前天和同事也討論對比過。

顯然2者對比是t1和t2+t3的對比,在這種情況下由於t2的時間消耗導致t1反過來可能比t2+t3的時間要少。

因為t3=t1/2,而t2可能會&>t3。

50%的變更率實際上是一個非常大的變更,在現實環境中非常難以見到,所以還沒引起足夠的重視和優化。

當更多問題暴露出來的時候,需求就會有了,那麼此時應該增加一種設計介面:即在知道明顯某個區域是大變化,應該省略diff直接進行重繪的時候,提供兼容易用的API顯示聲明這裡別再diff,直接toString()渲染即可。

另外react的vd對於text節點類型的span封裝也造成了一些性能浪費,即便不考慮DOM侵入和語義、css干擾。

innerHTML: dbmon (innerHTML)

react: dbmon (react)

migi: dbmon (migi)


我看了很多答案感覺都在發散思維,沒答到點子上。問題是

網上都說操作真實 DOM 慢,但測試結果卻比 React 更快,為什麼?

先說說為什麼網上都說操作真實 DOM 慢,只能說網上的人並沒有騙你,因為操作 DOM 會導致頁面(局部)重繪,而這個重繪是最耗性能的。如果你寫過遊戲引擎你就知道將一幀渲染到屏幕的效率基本上是影響 fps 的瓶頸,而不是遊戲數據的修改。所以說 DOM 操作慢,是說的比單純的邏輯運算慢

然後這個測試結果為什麼 vanilla 會比 React 快?這個測試實際上就是測一個 data 數組內全部數據更改後重繪的時間。這個測試中所比較的包括 vanilla 在內的框架,其實都是做了數據綁定這件事,什麼意思,就是為了讓開發者把精力集中在操作數據,而接管 DOM 操作

React 的 Virtual DOM 在這個測試中所做的事情是在數據更新後對修改前後的兩棵虛擬 DOM 樹進行 diff 差異對比,根據 React 的 diff 演算法,對比數組長度沒有變化,並且數組元素全是基本元素(div, span),那麼它將直接修改這些 DOM 元素內的 Text 節點,實際上做了 n 次 DOM 操作,併產生一次 container 的重繪(因為同一 tick 內 container 中的每個子節點都發生了改變)。

而 vanilla 所做的事情很簡單,每次數據變更後,拼接 html,然後填充到 container 的 innerHtml,實際上只做了一次 DOM 操作,併產生一次 container 的重繪

所以,在這個測試中,React 和 vanilla 的 DOM 操作時間其實是差別不大。雖然 vanilla 只有一次 DOM 操作,但是他在瀏覽器引擎內會分解成 n 次節點創建,但是這總是比腳本級別的 javascript 做 n 次節點修改要快一點。即使兩者的 DOM 操作耗時一樣,React 依然會比 vanilla 慢一點,因為 React 在邏輯上還做了虛擬 DOM 操作創建,diff 差異對比和 reconciliation 調和差異

--------------------- 以上為解答,以下吹水 ----------------------

其實這個測試根本不能左右你選擇使用 React 還是 vanilla,因為在實際的開發中整個數據的改變發生的頻率不會很大,即使 Angular 這麼慢,在偶爾發生一次整體數據變更的時候,這點時間還是可以忍受的。React 主要解決的是局部數據變更引起的重繪成本,這時 React 通過調和演算法可以分析出比較小的區域重繪的 DOM 操作方法,而使用 vanilla 或者其他模版庫,就只能整塊重繪了。

參考:

ARV 渲染實現比較之 React

ARV 渲染實現比較總結


大家都忽略了一個問題,那就是原作者的 React 寫得很爛。

我重寫了 React 的部分,結果會有趣一些:

Benchmark:Performance Comparison for Knockout, Angular and React

Source code:Huxpro/React-benchmark · GitHub

正如 @noyobo 所說,題主貼出的 Benchmark,如果第二次 run React ,時間會長到幾千 ms ,這肯定不對勁啊。

玩玩重寫後的版本你就會發現,

第一次:

第二次:

第一次渲染,React 需要建立 Virtual DOM 後再渲染真實 DOM ,當然比 Raw 慢。

而之後的每一次渲染,React 都要快過 Raw,這才是 React diff 機制 Awesome 的地方

(具體參見 @尤雨溪 答案)


你的問題就是錯的,從多個層面上來說都是錯的。

  • 首先,這個測試顯示的數字上,是否 react 真的慢於原生?只有前兩次是這樣,後面你測一千次也是 react 更快。
  • 其次,這個顯示的數字是否代表真實的速度?並不是這樣。實際表現,原生更慢。
    • 羅志宇:網上都說操作真實 DOM 慢,但測試結果卻比 React 更快,為什麼? 這個回答講的已經很清楚了,(就這個測試而言)原生花在 rendering 上的時間相當多,你可以自己用 Chrome 開發者工具的 performance 測測看。
    • 我這裡的數據是,react 的版本,rendering 小於 scripting,原生的 rendering 是 scripting 的 1.5 倍以上。本來 react 顯示的數字就比原生小,我這裡是 30ms 對 50ms(左右),這麼一算總時間就是 60ms 對 125ms。
    • 這個數字是真的假的,很容易就能感受到,你用滑鼠去飛速點擊,react 的毫無卡頓,但原生的就會卡卡的。
  • 最後,這個程序能否代表真實 DOM 操作?並不能。
    • 原生肯定不會比 react 慢,比如這裡你可以第一次渲染的時候把列表裡的 DOM 都存起來,以後每次渲染遍歷一遍 DOM,直接設置它們的 innertHTML。在我這裡這麼做只要 20ms。
    • 測試里這段代碼著實太惡劣了(見下),生成一段巨大的 HTML,讓瀏覽器去解析,這是可能都算不上 DOM 操作了。這麼做相當於寫一段程序,展示一段數據,你不是留一個介面,直接讓人去改內存,而是把數據硬編碼在程序里,每次要刷新就重新編譯一遍。
    • 哪怕是生成一個 DOM,循環的時候 clone 它,然後讓 container 去 appendChild,都要比這個來得好,基本可以做到 react 那樣不卡(我這裡顯示是 35ms)。(有趣的是這種做法在 profile 的時候性能急劇下降,不知道為什麼)

for (var i = 0; i &< data.length; i++) { var render = template; render = render.replace("{{className}}", ""); render = render.replace("{{label}}", data[i].label); html += render; } container.innerHTML = html;


我用chrome試驗, 多次不停的點刷新React 第一次run 54ms, 不停點擊刷新維持在30左右

Raw 第一次run 12ms, 不停點擊刷新維持在70ms左右


@kidneyball 的回答挺詳細的了。先理解「虛擬DOM」的概念。

react這個「虛擬DOM」的性能優越不是體現在 初始化第一次創建DOM,而是更加精確更加有效率地操作DOM。

例如,ul下面有二十個li,現在需要更新這些li,coding的時候工程師往往因為「偷懶」會把原來的li先remove掉,再加入新的。但用到react,還是一樣的操作,它會把前後做對比,精確地更新不同的部分。說的第一次慢,是因為它要創建虛擬DOM結構的第一個版本。

另外,如果工程師精確地修改前後不一樣的部分,效率可以比react更好,因為少了對比這個過程。


看了一下你給的鏈接,第一次 run 的時候確實是 raw 快,但是如果 run 多次,react 速度還是領先的。樓上意見分析的很透徹了,以下我就簡單說明,供懶人參考

沒有仔細閱讀頁面源代碼,僅僅從現象分析下原因(歡迎大家交流)。

第一次 run 的時候,react 和 raw 都需要插入 dom,react 還要在 virtual dom 里渲染一遍,再 diff 到真實 dom,這就比 raw 花費了更長時間。

而當你第二次運行的時候,raw 就沒有 react 快了,raw 是刪除了所有元素,再添加,而 react 只是把 diff 的元素更新了,react 速度會快。實際測試也符合這個猜想,我本地第二次測試react 花費28ms,raw 花費53ms。

virtual dom還是適合頻繁部分修改 dom 的場景,純插入和刪除可能性能不如 raw。

一個簡單的理解可以認為 virtual dom 就是一個 cache。最壞情況下就是 cache 每次都沒命中,肯定還沒有不加 cache 速度快 。cache 的原理是利用數據的時空局部性原理,將熱數據放到高速存儲區,只有能多次重複利用 cache 時,才能體現 cache 的優勢。(放在這裡就是頻繁部分修改 dom,才能發揮react的優勢)

點贊加關注,乾貨持續分享中


舉個例子,實際的。

富文本編輯器中的粘貼功能,取到字元串的html字元。ctrl v的時候你需要分析這一段html字元串中的節點信息,比如是否格式化,是否重置style樣式,然後再創建節點插入頁面。完成複製。

這一過程中無疑使用dom api完成分析比較快捷。正則錯誤率高還複雜。

測試無論chrome還是ie 原生api的方法都要比虛擬dom慢。比如getTagname getStyle 都是非常消耗性能的 比如獲取前後節點信息 父子節點信息。

這時虛擬dom也就是假的parent child next prev getTags 等方法速度可是原生的很多倍。。尤其複雜關係結構。

我只說這一個例子。。手機打的 別見怪。。。方法寫的不全。

ps 沒用react 但是用過虛擬dom優化


在操作 DOM 上,都認為是一樣的。不一樣的是通過什麼策略去操作。舉例:react 如果在頂樹刷新 state ,即使 vdom diff 了也可能造成不必要的檢測和刷新。通過假設跨級操作等數學假設的方式,設置 key,減少判斷和檢索,樹的 patch,都是一些策略。真實操作 dom 的代價都是一樣的~也就是同樣一直刷 dom的前提下,框架帶不來收益,收益在於怎麼避免不需要的操作。


什麼邏輯,那些框架最終還不是要操作DOM。。。


個人覺得這種快應該這樣理解:

在傳統的jquery開發中,大家都很隨意,大多數程序員都不會全面考慮dom操作慢的問題的,由於種種原因,寫出各種混亂的直接操作dom的代碼,這肯定會造成最終產生的應用變得比較慢。

而react對dom操作做了一層優化,逼迫開發者按照他的方式去做界面操作,以便react做集中的優化處理,這樣做出來的應用,很可能就比較快。

所以總結起來就是說,用react寫出來的應用更容易變得比較快。


框架都是用來解決「人類」的問題的,而不是機器的問題

框架存在的意義在於,在它的適用範圍內,保證了較好性能的前提下,提供了更好的開發體驗(人的問題)

明白了這一點之後,回到樓主的問題,react也好,vue也好,都是設計為瀏覽器中工作在view層上的一種解決方案,這必然涉及大量的DOM操作,正是這個「大量」的存在,才使「優化」成為了一種必需

看看你的例子,只有一個.innerHTML的操作,簡到極致了,如何優化呢? 任何優化都變成了一種負擔,一種畫蛇添足


推薦閱讀:

2017 年底如何比較 Angular 4, React 16, Vue 2 的開發和運行速度?
如何理解 Vue.JS 2016年的 github 星標( Star )數量增長超過 React ?
react中createFactory, createClass, createElement分別在什麼場景下使用,為什麼要這麼定義?
如何評價React v16.0?
阿里還會使用react嗎?

TAG:前端開發 | JavaScript | React |