漫談前端性能 突破 React 應用瓶頸

React 狀態管理與同構實戰

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

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

這篇文章咱們從瀏覽器和 JavaScript 引擎角度來剖析前端性能,同時創新 React,充分利用瀏覽器能力突破侷限。java


在文章開始以前,我想先向你們介紹一本書。react

從去年起,我和知名技術大佬顏海鏡開始了合著之旅,今年咱們共同打磨的書籍《React 狀態管理與同構實戰》終於正式出版了!這本書以 React 技術棧爲核心,在介紹 React 用法的基礎上,從源碼層面分析了 Redux 思想,同時着重介紹了服務端渲染和同構應用的架構模式。書中包含許多項目實例,不只爲用戶打開了 React 技術棧的大門,更能提高讀者對前沿領域的總體認知。git

若是各位對圖書內容或接下來的內容感興趣,還望多多支持!文末有詳情,不要走開!github


性能問題的阿喀琉斯之踵

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

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

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

JavaScript 主線程

圖中的幾個關鍵角色:瀏覽器

Call Stack:調用棧,即 JavaScript 代碼執行的地方,Chrome 和 NodeJS 中對應 V8 引擎。當它執行完當前全部任務時,棧爲空,等待接收 Event Loop 中 next Tick 的任務。

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

Event queue: 每次經過 AJAX 或者 setTimeout 添加一個異步回調時,回調函數通常會加入到 Event queue 當中。

Job queue: 這是預留給 promise 且優先級較高的通道,表明着「稍後執行這段代碼,可是在 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 tick 週期執行,進而主線程就能夠在子任務間隙當中執行 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 的性能瓶頸。

這並不難理解,由於 DOM 更新只是 JavaScript 調用瀏覽器的 APIs,這個過程對全部框架以及原生 JavaScript 來說是同樣黑盒執行的,這一部分的性能消耗是同等且不可避免的。

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

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

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

實例

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

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

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

performance 圖示

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

這三種任務,總共佔用瀏覽器主線程 7s,此時間內瀏覽器沒法與用戶交互。主要是黃色部分執行時間較長,佔用了 6s,即 React 較長時間佔用主線程,致使主線程沒法響應用戶輸入。這就是一個典型的例子。

React 性能升級——React Fiber

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

更多關於 Fiber 的內容一樣推薦文章:React的新引擎—React Fiber是什麼? 文章中又在應用 React Fiber 的場景下,重複剛纔的例子,不會再出現頁面卡頓,交互天然而順暢。

瀏覽器主線程的 performance 以下圖所示:

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,這也吸引來了 Dan Abramov 的討論。雖然這樣的提案被拒絕,但這並不妨礙咱們讓 React 結合 worker 作試驗。

Talk is cheap, show me the code, and demo: 讀者能夠訪問這裏,該網站分別用原生 React 和接入 Web worker 版 React 實現了兩個應用,並對比其性能表現。關於代碼部分,感興趣的同窗能夠私信我。

最終結論:只有當大量的節點發生變化的時,Web worker 提高渲染性能纔會有一些效果。當節點數量很是少的時候,接入 Web worker 的性能多是負收益。我認爲這是因爲 worker 線程和主線程之間的通訊成本所致。

這麼看,Web worker 版本的 React 仍有性能提高空間,我簡單總結以下:

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

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

  • 使用 postMessage 傳遞消息時,採用 transferable objects 進行數據負載
  • 關於 worker 版 syntheticEvent

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

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

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

Redux 和 Web worker

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

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

  • 一個實時每 16 毫秒,顯示計數(每秒增長 1)的 blinker 模塊;

  • 一個定時每 500 毫秒,更新背景顏色的 counter 模塊;

  • 一個永久往復運動的 slider 模塊;

  • 一個每 16 毫秒翻轉 5 度的 spinner 模塊

如圖:

image.png

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

若是將 N-皇后計算放置到 worker 線程,咱們會發現 demo 展示了使人驚訝的性能提高,徹底絲滑毫無卡頓。如上圖,左半部分爲正常版本,不出意外出現了頁面卡頓,右側是接入 worker 以後的應用。

在實現層面,藉助 Redux 庫的 enchancer 設計,完成了抽象封裝。 一個 store enhancer,實際上就是一個 curry 化的高階函數,這和 React 中的高階組件的概念很類似,同時也相似咱們更加熟悉的中間件。其實參考 Redux 源碼,會發現 Redux 源碼中 applyMiddleware 方法的執行結果就是一個 store enhancer。

那麼爲何不選擇中間件,而是使用 enhancer 來實現呢?這個 Redux worker demo 所採用的公共庫設計思路很是有趣,關於神奇的 Redux 高階內容再也不展開,感興趣的讀者能夠在我新出版的書中找到相應內容。這也就到了廣告時間。。。


《React 狀態管理與同構實戰》這本書由我和前端知名技術大佬顏海鏡協力打磨,凝結了咱們在學習、實踐 React 框架過程當中的積累和心得。**除了 React 框架使用介紹之外,着重剖析了狀態管理以及服務端渲染同構應用方面的內容。**同時吸收了社區大量優秀思想,進行概括比對。

本書受到百度公司副總裁沈抖、百度資深前端工程師董睿,以及知名 JavaScript 語言專家阮一峯、Node.js 佈道者狼叔、Flarum 中文社區創始人 justjavac、新浪移動前端技術專家小爝、百度資深前端工程師顧軼靈等前端圈衆多專家大咖的聯協力薦。

有興趣的讀者能夠點擊這裏,瞭解詳情。也能夠掃描下面的二維碼購買。再次感謝各位的支持與鼓勵!懇請各位批評指正!

React 狀態管理與同構實戰

React 狀態管理與同構實戰

最後,前端學習永無止境,但願和每一位技術愛好者共同進步,你們能夠在知乎找到我!

Happy coding!

相關文章
相關標籤/搜索