摘要:表示調用棧在下一將要執行的任務。兩方性能解藥我們一般有兩種方案突破上文提到的瓶頸將耗時高成本高易阻塞的長任務切片,分成子任務,并異步執行這樣一來,這些子任務會在不同的周期執行,進而主線程就可以在子任務間隙當中執行更新操作。
性能一直以來是前端開發中非常重要的話題。隨著前端能做的事情越來越多,瀏覽器能力被無限放大和利用:從 web 游戲到復雜單頁面應用,從 NodeJS 服務到 web VR/AR、數據可視化,前端工程師總是在突破極限。隨之而來的性能問題有的被迎刃而解,有的成為難以逾越的盾墻。
那么,當我們在談論性能時,到底在說什么?基于 React 框架開發的應用,在性能上又有哪些特點?
這篇文章我們從瀏覽器和 JavaScript 引擎角度來剖析前端性能,同時創新 React,充分利用瀏覽器能力突破局限。
在文章開始之前,我想先向大家介紹一本書。
從去年起,我和知名技術大佬顏海鏡開始了合著之旅,今年我們共同打磨的書籍《React 狀態管理與同構實戰》終于正式出版了!這本書以 React 技術棧為核心,在介紹 React 用法的基礎上,從源碼層面分析了 Redux 思想,同時著重介紹了服務端渲染和同構應用的架構模式。書中包含許多項目實例,不僅為用戶打開了 React 技術棧的大門,更能提升讀者對前沿領域的整體認知。
如果各位對圖書內容或接下來的內容感興趣,還望多多支持!文末有詳情,不要走開!
性能問題的阿喀琉斯之踵事實上,性能問題多種多樣:瓶頸可能出現在網絡傳輸過程,造成前端數據呈現延遲;也可能是 hybrid 應用中,webview 容器帶來限制。但是在分析性能問題時,經常逃不開一個概念——JavaScript 單線程。
瀏覽器解析渲染 DOM Tree 和 CSS Tree,解析執行 JavaScript,幾乎所有的操作都是在主線程中執行。因為 JavaScript 可以操作 DOM,影響渲染,所以 JavaScript 引擎線程和 UI 線程是互斥的。換句話說,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”??梢钥吹剑?strong>頁面沒有任何響應。等待 7s 之后,輸入框中突然出現了之前輸入的 “hireact”。同時, BlockList 組件也更新了。
顯而易見,這樣的用戶體驗并不好。
瀏覽器主線程在這 7s 的 performance 如下圖所示:
黃色部分是 JavaScript 執行時間,也是 React 占用主線程時間;
紫色部分是瀏覽器重新計算 DOM Tree 的時間;
綠色部分是瀏覽器繪制頁面的時間。
這三種任務,總共占用瀏覽器主線程 7s,此時間內瀏覽器無法與用戶交互。主要是黃色部分執行時間較長,占用了 6s,即 React 較長時間占用主線程,導致主線程無法響應用戶輸入。這就是一個典型的例子。
React 性能升級——React FiberReact 核心團隊很早之前就預知性能風險的存在,并且持續探索可解決的方式?;跒g覽器對 requestIdleCallback 和 requestAnimationFrame 這兩個API 的支持,React 團隊實現新的調度策略 -- Fiber reconcile。
更多關于 Fiber 的內容同樣推薦文章:React的新引擎—React Fiber是什么?
文章中又在應用 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,這也吸引來了 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 模塊
如圖:
這些模塊都定時頻繁地更新 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、新浪移動前端技術專家小爝、百度資深前端工程師顧軼靈等前端圈眾多專家大咖的聯合力薦。
有興趣的讀者可以點擊這里,了解詳情。也可以掃描下面的二維碼購買。再次感謝各位的支持與鼓勵!懇請各位批評指正!
最后,前端學習永無止境,希望和每一位技術愛好者共同進步,大家可以在知乎找到我!
Happy coding!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108160.html
摘要:分析性能的影響但是需要注意時間單位,只是微秒而已,毫秒的千分之一秒的百萬分之一。在這種情況下,優化毫秒的性能隱患無異于撿了芝麻丟了西瓜。 同步自:https://sulin.me/2019/T2ZXZB.... 在分布式系統開發中,我們經常需要將各種各樣的狀態碼、錯誤信息傳遞給最外層的調用方,這個調用方通常是http/api接口,錯誤信息比如登錄失效、參數錯誤等等。 最外層接口暴露的...
摘要:的前生今世系統系統作為全球第一大系統,基于開發的移動端有著諸多的性能優勢。官方提供了豐富的原生接口封裝系統結構圖像處理引擎年圖像處理引擎成立,用來展示火狐和其他自家的產品使用。而語言早已突破階段,正穩步邁向階段。 showImg(https://segmentfault.com/img/remote/1460000018724305); Android 的前生今世 Android 系統...
閱讀 3236·2021-11-24 10:43
閱讀 4197·2021-11-24 10:33
閱讀 3771·2021-11-22 09:34
閱讀 2125·2021-10-11 10:58
閱讀 3732·2021-10-11 10:58
閱讀 859·2021-09-27 13:36
閱讀 3579·2019-08-30 15:54
閱讀 2965·2019-08-29 18:41