前端性能的本質是什麼

性能一直以來是前端開發中很是重要的話題。隨着前端能作的事情愈來愈多,瀏覽器能力被無限放大和利用:從 web 遊戲到複雜單頁面應用,從 NodeJS 服務到 web VR/AR 和數據可視化,前端工程師老是在突破極限。隨之而來的性能問題有的被迎刃而解,有的成爲難以逾越的盾牆。前端

那麼,當咱們在談論性能時,到底在說什麼?基於 React 框架開發的應用,在性能上又有哪些特色?react

前端性能的本質是什麼,你有真正瞭解嗎?

 

性能問題本質git

事實上,性能問題多種多樣:瓶頸可能出如今網絡傳輸過程,形成前端數據呈現延遲;也多是移動 hybrid 應用中,wbview 容器帶來了瓶頸和限制。可是在分析性能問題時,常常逃不開一個概念——JavaScript 單線程。github

瀏覽器解析渲染 DOM Tree 和 CSS Tree,解析執行 JavaScript,幾乎全部的操做都是在主線程中執行。由於 JavaScript 能夠操做 DOM,影響渲染,因此 JavaScript 引擎線程和 U I線程是互斥的。換句話說,JavaScript 代碼執行時會阻塞頁面的渲染。web

經過下面的圖示來進行了解:算法

前端性能的本質是什麼,你有真正瞭解嗎?

 

圖中的幾個關鍵角色:promise

Call Stack:調用棧,即 JavaScript 代碼執行的地方,Chrome 和 NodeJS 中對應 V8 引擎。遵循 LIFO(last-in-first-out)原則。當執行完當前全部任務時,棧爲空,等待接收 Event Loop 中 next Tick 的任務。瀏覽器

Browser APIs:這是鏈接 JavaScript 代碼和瀏覽器內部的橋樑,使得 JavaScript 代碼能夠經過 Browser APIs 操做 DOM,調用 setTimeout,AJAX 等。網絡

Event queue: 每次經過 AJAX 或者 setTimeout 添加一個回調時,回調函數會加入到 Event queue 當中。前端工程師

Job queue: 這是預留給 promise 且優先級較高的 queue,表明着「稍後執行這段代碼,可是在 next Event Loop tick 以前執行」。它屬於 ES 規範,注意區別對待,這裏暫不展開。

Next Tick: 表示調用棧 call stack 在下一 tick 將要執行的任務。它由一個 Event queue 中的回調,所有的 job queue,部分或者所有 render queue 組成。注意 current tick 只會在 Job queue 爲空時纔會進入 next tick。這就涉及到 task 優先級了,可能你們對於 microtask 和 macrotask 更加熟悉。

Event Loop: 它會「監視」(輪詢)call stack 是否爲空,call stack 爲空時將會由 Event Loop 推送 next tick 中的任務到 call stack 中。

在瀏覽器主線程中,JavaScript 代碼在調用棧 call stack 執行時,可能會調用瀏覽器的 API,對 DOM 進行操做。也可能執行一些異步任務:這些異步任務若是是以回調的方式處理,那麼每每會被添加到 Event queue 當中;若是是以 promise 處理,就會先放到 Job queue 當中。這些異步任務和渲染任務將會在下一個時序當中由調用棧處理執行。

理解了這些,你們就會明白:若是調用棧 call stack 運行一個很耗時的腳本,好比解析一個圖片,call stack 就會像北京上下班高峯期的環路入口同樣,被這個複雜任務堵塞。進而阻塞 UI 響應,主線程其餘任務都要排隊。這時候用戶點擊、輸入、頁面動畫等都沒有了響應。

這樣的性能瓶頸,就如同阿喀琉斯之踵同樣,在必定程度上限制着 JavaScript 的發揮。

江湖救急——兩方性能解藥

咱們通常有兩種方案突破上文提到的瓶頸:

將耗時高、成本高的長任務切片,分紅子任務,並異步執行

這樣一來,這些子任務會在不一樣的 call stack 週期執行,進而主線程就能夠在子任務間隙當中執行 UI 更新操做。設想常見的一個場景:若是咱們須要渲染一個很長的列表,列表由十萬條數據組成,那麼相比一次性渲染所有數據內容,咱們能夠將數據分段,使用 setTimeout API 去分步處理,構建列表的工做就被分紅了不一樣的子任務在瀏覽器中執行,在這些子任務間隙,瀏覽器得以處理 UI 更新。

另一個創新性的作法:使用 HTML5 Web Worker

Web Worker 容許咱們將 JavaScript 腳本在不一樣的瀏覽器線程中執行。所以,一些耗時的計算過程咱們均可以放在 Web Worker 開啓的線程當中處理。下文會有詳解。

React 框架性能剖析

社區上關於 React 性能的內容每每聚焦在業務層面,主要是使用框架的「最佳實踐」。這裏咱們不去談論「使用 shoulComponentUpdate 減小沒必要要的渲染」,「減小 render 函數中 inline-function」等「老生常談」的話題,本文會從 React 框架實現層面分析其性能瓶頸和突破策略。

原生的 JavaScript 必定是最高效的,這個毫無爭議。相比其餘框架,React 在 JavaScript 執行層面花費的時間較多,這顯然是由於 Virtual DOM 構建,以及計算 DOM diff,生成 render patch 一系列複雜過程所形成的。也就是說 React 著名的調度策略 -- stack reconcile 是 React 的性能瓶頸。

這並不難理解,由於 UI 渲染只是 JavaScript 調用瀏覽器的 APIs,這個過程對全部框架以及原生 JavaScript 來說是同樣的,都是黑盒執行,這一部分的性能消耗是且沒法取巧的。

再看咱們的 React,stack reconcile 過程會深度優先遍歷全部的 Virtual DOM 節點,進行 diff。整棵 Virtual DOM 計算完成以後,纔將任務出棧釋放主線程。因此,瀏覽器主線程被 React 更新狀態任務佔據的時候,用戶與瀏覽器進行任何的交互都不能獲得反饋,只有等到任務結束,才能忽然獲得瀏覽器的響應。

咱們來看一個典型的場景,來自文章「React的新引擎—React Fiber是什麼?」

(http://www.infoq.com/cn/articles/what-the-new-engine-of-react)

這個例子會在頁面中建立一個輸入框,一個按鈕,一個 BlockList 組件。BlockList 組件會根據 NUMBER_OF_BLOCK 數值渲染出對應數量的數字顯示框,數字顯示框顯示點擊按鈕的次數。

前端性能的本質是什麼,你有真正瞭解嗎?

 

在這個例子中,咱們能夠設置 NUMBER_OF_BLOCK 的值爲 100000,將其變爲一個「複雜」的網頁。 點擊按鈕,觸發 setState,頁面開始更新。此時點擊輸入框,輸入一些字符串,好比 「hi,react」。能夠看到,頁面沒有任何的響應。等待 7s 以後,輸入框中忽然出現了以前輸入的 「hireact」。同時, BlockList 組件也更新了。

顯而易見,這樣的用戶體驗並很差。

將瀏覽器主線程在這 7s 的 performance 以下圖所示:

前端性能的本質是什麼,你有真正瞭解嗎?

 

黃色部分是 JavaScript 執行時間,也是 React 佔用主線程時間,紫色部分是瀏覽器從新計算 DOM Tree 的時間,綠色部分是瀏覽器繪製頁面的時間。

三種任務,佔用瀏覽器主線程 7s,此時間內瀏覽器沒法與用戶交互。可是DOM 改變以後,瀏覽器從新計算 DOM Tree,重繪頁面是一個必不可少的階段(紫色綠色階段)。主要是黃色部分執行時間較長,佔用了 6 s,即 React 較長時間佔用主線程,致使主線程沒法響應用戶輸入。

此處場景內容選自文章「React的新引擎—React Fiber是什麼?」

React 性能——React Fiber

React 核心團隊很早以前就預知性能風險的存在,而且持續探索可解決的方式。基於瀏覽器對 requestIdleCallback 和 requestAnimationFrame 這兩個API 的支持,React 團隊實現新的調度策略 -- Fiber reconcile。

在應用 React Fiber 的場景下,再重複剛纔的例子。瀏覽器主線程的 performance 以下圖所示:

前端性能的本質是什麼,你有真正瞭解嗎?

 

能夠看到,在黃色 JavaScript 執行過程當中,也就是 React 佔用瀏覽器主線程期間,瀏覽器在也在從新計算 DOM Tree,而且進行重繪,截圖顯示,瀏覽器渲染的就是用戶新輸入的內容。簡單說,在 React 佔用瀏覽器主線程期間,瀏覽器也在與用戶交互。這顯然是「更好的性能」體現。

以上是 React 「將耗時高的任務分段」作法,下面咱們再來看另外一種「民間」作法,體現 Web Worker 應用。

React結合Web Worker

關於 Web Worker 的概念此文再也不贅述,你們能夠訪問 MDN 地址進行了解。咱們聚焦思考點:若是讓 React 接入 Web Worker 的話,切入點在哪裏,如何實施?

總所周知,標準的 React 應用由兩部分構成:

  • React core:負責絕大部分的複雜的 Virtual DOM 計算;
  • React-Dom:負責與瀏覽器真實 DOM 交互來展現內容。

那麼答案很簡單,咱們嘗試在 Web Worker 中運行 React Virtual DOM 的相關計算,而不是傳統的在主線程中進行。即將 React core 放入 Web Worker 線程中。

也確實有人提出了這樣的想法,請參考 React 倉庫第 #3092 號 Issue,這樣的提議遭到了 React 官方的禮貌回絕:

「Relay in a worker on the other hand seems very plausible.」

具體緣由能夠在此 issue 中找到,內容不少,也吸引來了 Dan Abramov 的現身說法,固然若是我是 React 庫的開發者,我也不會接受這樣的變更。不過這並不妨礙咱們讓 React 結合 Worker 作試驗。

Talk is cheap, show me the code, and demo: 讀者能夠訪問

http://web-perf.github.io/react-worker-dom/,

該網站分別用原生 React 和接入 Web Worker 版 React 實現了兩個應用,並對比其性能表現。

最終結論:不能絕對的說 Web Worker 能夠對渲染速率有大幅度提高。只有當大量的節點發生變化的時,Web Worker 提高渲染性能纔會有一些效果。實際上,當節點數量很是少的時候,Web Worker 的性能可能還不如 React 自己實現。這是因爲 worker 線程和主線程之間的通訊成本所致。

所以,Web Worker 版本的 React 仍有提高空間,我簡單總結以下:

• 由於 worker 線程和主線程在使用 postMessage 通訊時,成本較大,咱們能夠採用 batching 思想減小通訊的次數。

若是在每次 DOM 須要改變時,都調用 postMessage 通知主線程,不是特別明智。因此能夠用 batching 思想,將 worker 線程中計算出來的 DOM 待更新內容進行收集,再統一發送。這樣一來,batching 的粒度就頗有意思了。若是咱們走極端,每次 batching 收集的變動都很是多,那麼在一次 batching 時就給瀏覽器真正的渲染過程帶來了壓力,反而拔苗助長。

• 使用 postMessage 傳遞消息時,採用 transferable objects 進行數據負載

在 worker 和主線程之間,我想要傳遞的數據可能不是一個穩定的結構,所以,我須要制定一個公共的協議。使用 transferable objects 傳遞信息,可以有效提升效率。更多內容參見社區文檔。

• 關於 Worker 版 syntheticEvent

原生 React 有一套 Event System 在最頂層監聽全部的瀏覽器事件,將它們轉化爲合成事件,傳遞給咱們在 Virtual DOM 上定義的事件監聽者。

對於咱們的 Web Worker,因爲 web Worker 不能直接操做 DOM,也就是說不能監聽瀏覽器事件。所以全部事件一樣都在主線程中處理,轉化爲虛擬事件並傳遞給 worker 線程,也就意味着全部關於建立虛擬事件的操做仍是都在主線程中進行,一個可能改善的方案是,能夠直接將原始事件傳遞給 worker,由 worker 來生成模擬事件並冒泡傳遞。

關於 React 結合 worker 還有不少值得深挖的內容,好比事件處理方面 preventDefault 和 stopPropogation 的同步性;使用 multiple worker(一個以上 worker)探究等,若是讀者有興趣,我會專門寫篇文章介紹。

Redux和Web Worker

既然 React 能夠接入 Web Worker,狀態管理工具 Redux 固然也能借鑑這樣的思想,將 Redux 中 reducer 複雜的純計算過程放在 worker 線程裏,是否是一個很好的思路?

我使用 「N-皇后問題」 模擬大型計算,除了這個極其耗時的算法,頁面中還運行這麼幾個模塊來實現渲染邏輯:

  • 一個實時每 16 毫秒,顯示計數(每秒增長 1)的 blinker 模塊;
  • 一個定時每 500 毫秒,更新背景顏色的 counter 模塊;
  • 一個永久往復運動的 slider 模塊;
  • 一個每 16 毫秒翻轉 5 度的 spinner 模塊
前端性能的本質是什麼,你有真正瞭解嗎?

 

這些模塊都定時頻繁地更新 DOM 樣式,進行渲染。正常狀況下,當 JavaScript 主線程進行 N-皇后計算時,這些渲染過程都將被卡頓。

若是將 N-皇后計算放置到 worker 線程,咱們會發現 demo 展示了使人驚訝的性能提高,徹底絲滑毫無卡頓。

以下圖,左邊爲正常版本,不出意外出現了頁面卡頓,右側是介入 worker 以後的應用:

前端性能的本質是什麼,你有真正瞭解嗎?

 

在實現層面,藉助 Redux 庫的 enchancer 設計,完成了抽象封裝(相似中間件)。 一個 store enhancer,實際上就是一個顆粒化的高階函數,最終返回值是一個能夠建立功能更增強大的 store 的函數 (enhanced store creator),這和 React 中的高階組件的概念很類似,同時也相似咱們更加熟悉的中間件,其實參考 Redux 源碼,會發現 Redux 源碼中 applyMiddleware 方法,applyMiddleware(...middlewares) 的執行結果就是一個 store enhancer。

相關文章
相關標籤/搜索