摘要:表示調(diào)用棧在下一將要執(zhí)行的任務(wù)。兩方性能解藥我們一般有兩種方案突破上文提到的瓶頸將耗時高成本高易阻塞的長任務(wù)切片,分成子任務(wù),并異步執(zhí)行這樣一來,這些子任務(wù)會在不同的周期執(zhí)行,進而主線程就可以在子任務(wù)間隙當中執(zhí)行更新操作。
性能一直以來是前端開發(fā)中非常重要的話題。隨著前端能做的事情越來越多,瀏覽器能力被無限放大和利用:從 web 游戲到復(fù)雜單頁面應(yīng)用,從 NodeJS 服務(wù)到 web VR/AR、數(shù)據(jù)可視化,前端工程師總是在突破極限。隨之而來的性能問題有的被迎刃而解,有的成為難以逾越的盾墻。
那么,當我們在談?wù)撔阅軙r,到底在說什么?基于 React 框架開發(fā)的應(yīng)用,在性能上又有哪些特點?
這篇文章我們從瀏覽器和 JavaScript 引擎角度來剖析前端性能,同時創(chuàng)新 React,充分利用瀏覽器能力突破局限。
在文章開始之前,我想先向大家介紹一本書。
從去年起,我和知名技術(shù)大佬顏海鏡開始了合著之旅,今年我們共同打磨的書籍《React 狀態(tài)管理與同構(gòu)實戰(zhàn)》終于正式出版了!這本書以 React 技術(shù)棧為核心,在介紹 React 用法的基礎(chǔ)上,從源碼層面分析了 Redux 思想,同時著重介紹了服務(wù)端渲染和同構(gòu)應(yīng)用的架構(gòu)模式。書中包含許多項目實例,不僅為用戶打開了 React 技術(shù)棧的大門,更能提升讀者對前沿領(lǐng)域的整體認知。
如果各位對圖書內(nèi)容或接下來的內(nèi)容感興趣,還望多多支持!文末有詳情,不要走開!
性能問題的阿喀琉斯之踵事實上,性能問題多種多樣:瓶頸可能出現(xiàn)在網(wǎng)絡(luò)傳輸過程,造成前端數(shù)據(jù)呈現(xiàn)延遲;也可能是 hybrid 應(yīng)用中,webview 容器帶來限制。但是在分析性能問題時,經(jīng)常逃不開一個概念——JavaScript 單線程。
瀏覽器解析渲染 DOM Tree 和 CSS Tree,解析執(zhí)行 JavaScript,幾乎所有的操作都是在主線程中執(zhí)行。因為 JavaScript 可以操作 DOM,影響渲染,所以 JavaScript 引擎線程和 UI 線程是互斥的。換句話說,JavaScript 代碼執(zhí)行時會阻塞頁面的渲染。
通過下面的圖示來進行了解:
圖中的幾個關(guān)鍵角色:
Call Stack:調(diào)用棧,即 JavaScript 代碼執(zhí)行的地方,Chrome 和 NodeJS 中對應(yīng) V8 引擎。當它執(zhí)行完當前所有任務(wù)時,棧為空,等待接收 Event Loop 中 next Tick 的任務(wù)。
Browser APIs:這是連接 JavaScript 代碼和瀏覽器內(nèi)部的橋梁,使得 JavaScript 代碼可以通過 Browser APIs 操作 DOM,調(diào)用 setTimeout,AJAX 等。
Event queue: 每次通過 AJAX 或者 setTimeout 添加一個異步回調(diào)時,回調(diào)函數(shù)一般會加入到 Event queue 當中。
Job queue: 這是預(yù)留給 promise 且優(yōu)先級較高的通道,代表著“稍后執(zhí)行這段代碼,但是在 next Event Loop tick 之前執(zhí)行”。它屬于 ES 規(guī)范,注意區(qū)別對待,這里暫不展開。
Next Tick: 表示調(diào)用棧 call stack 在下一 tick 將要執(zhí)行的任務(wù)。它由一個 Event queue 中的回調(diào),全部的 job queue,部分或者全部 render queue 組成。注意 current tick 只會在 Job queue 為空時才會進入 next tick。這就涉及到 task 優(yōu)先級了,可能大家對于 microtask 和 macrotask 更加熟悉,這里不再展開。
Event Loop: 它會“監(jiān)視”(輪詢)call stack 是否為空,call stack 為空時將會由 Event Loop 推送 next tick 中的任務(wù)到 call stack 中。
在瀏覽器主線程中,JavaScript 代碼在調(diào)用棧 call stack 執(zhí)行時,可能會調(diào)用瀏覽器的 API,對 DOM 進行操作。也可能執(zhí)行一些異步任務(wù):這些異步任務(wù)如果是以回調(diào)的方式處理,那么往往會被添加到 Event queue 當中;如果是以 promise 處理,就會先放到 Job queue 當中。這些異步任務(wù)和渲染任務(wù)將會在下一個時序當中由調(diào)用棧處理執(zhí)行。
理解了這些,大家就會明白:如果調(diào)用棧 call stack 運行一個很耗時的腳本,比如解析一個圖片,call stack 就會像北京上下班高峰期的環(huán)路入口一樣,被這個復(fù)雜任務(wù)堵塞。主線程其他任務(wù)都要排隊,進而阻塞 UI 響應(yīng)。這時候用戶點擊、輸入、頁面動畫等都沒有了響應(yīng)。
這樣的性能瓶頸,就如同阿喀琉斯之踵一樣,在一定程度上限制著 JavaScript 的發(fā)揮。
兩方性能解藥我們一般有兩種方案突破上文提到的瓶頸:
將耗時高、成本高、易阻塞的長任務(wù)切片,分成子任務(wù),并異步執(zhí)行
這樣一來,這些子任務(wù)會在不同的 call stack tick 周期執(zhí)行,進而主線程就可以在子任務(wù)間隙當中執(zhí)行 UI 更新操作。設(shè)想常見的一個場景:如果我們需要渲染一個由十萬條數(shù)據(jù)組成的列表,那么相比一次性渲染全部數(shù)據(jù),我們可以將數(shù)據(jù)分段,使用 setTimeout API 去分步處理,構(gòu)建渲染列表的工作就被分成了不同的子任務(wù)在瀏覽器中執(zhí)行。在這些子任務(wù)間隙,瀏覽器得以處理 UI 更新。
另外一個創(chuàng)新性的做法:使用HTML5 Web worker
Web worker 允許我們將 JavaScript 腳本在不同的瀏覽器線程中執(zhí)行。因此,一些耗時的計算過程我們都可以放在 Web worker 開啟的線程當中處理。下文會有詳解。
React 框架性能剖析社區(qū)上關(guān)于 React 性能的內(nèi)容往往聚焦在業(yè)務(wù)層面,主要是使用框架的“最佳實踐”。這里我們不去談?wù)摗笆褂?shoulComponentUpdate 減少不必要的渲染”、“減少 render 函數(shù)中 inline-function”等已經(jīng)“老生常談”的話題,本文主要從 React 框架實現(xiàn)層面分析其性能瓶頸和突破策略。
原生 JavaScript 一定是最高效的,這個毫無爭議。相比其他框架,React 在 JavaScript 執(zhí)行層面花費的時間較多,這是因為:
Virtual DOM 構(gòu)建 -> 計算 DOM diff -> 生成 render patch
這一系列復(fù)雜過程所造成的。也就是說,在一定程度上:React 著名的調(diào)度策略 -- stack reconcile 是 React 的性能瓶頸。
這并不難理解,因為 DOM 更新只是 JavaScript 調(diào)用瀏覽器的 APIs,這個過程對所有框架以及原生 JavaScript 來講是一樣黑盒執(zhí)行的,這一部分的性能消耗是同等且不可避免的。
再來看我們的 React:stack reconcile 過程會深度優(yōu)先遍歷所有的 Virtual DOM 節(jié)點,進行 diff。整棵 Virtual DOM 計算完成之后,將任務(wù)出棧釋放主線程。所以,瀏覽器主線程被 React 更新狀態(tài)任務(wù)占據(jù)的時候,用戶與瀏覽器進行任何交互都不能得到反饋,只有等到任務(wù)結(jié)束,才能得到瀏覽器的響應(yīng)。
我們來看一個典型的場景,來自文章:React的新引擎—React Fiber是什么?
這個例子會在頁面中創(chuàng)建一個輸入框,一個按鈕,一個 BlockList 組件。BlockList 組件會根據(jù) NUMBER_OF_BLOCK 數(shù)值渲染出對應(yīng)數(shù)量的數(shù)字顯示框,數(shù)字顯示框顯示點擊按鈕的次數(shù)。
在這個例子中,我們可以設(shè)置 NUMBER_OF_BLOCK 的值為 100000。這時候點擊按鈕,觸發(fā) setState,頁面開始更新。此時點擊輸入框,輸入一些字符串,比如 “hi,react”??梢钥吹剑?strong>頁面沒有任何響應(yīng)。等待 7s 之后,輸入框中突然出現(xiàn)了之前輸入的 “hireact”。同時, BlockList 組件也更新了。
顯而易見,這樣的用戶體驗并不好。
瀏覽器主線程在這 7s 的 performance 如下圖所示:
黃色部分是 JavaScript 執(zhí)行時間,也是 React 占用主線程時間;
紫色部分是瀏覽器重新計算 DOM Tree 的時間;
綠色部分是瀏覽器繪制頁面的時間。
這三種任務(wù),總共占用瀏覽器主線程 7s,此時間內(nèi)瀏覽器無法與用戶交互。主要是黃色部分執(zhí)行時間較長,占用了 6s,即 React 較長時間占用主線程,導(dǎo)致主線程無法響應(yīng)用戶輸入。這就是一個典型的例子。
React 性能升級——React FiberReact 核心團隊很早之前就預(yù)知性能風險的存在,并且持續(xù)探索可解決的方式?;跒g覽器對 requestIdleCallback 和 requestAnimationFrame 這兩個API 的支持,React 團隊實現(xiàn)新的調(diào)度策略 -- Fiber reconcile。
更多關(guān)于 Fiber 的內(nèi)容同樣推薦文章:React的新引擎—React Fiber是什么?
文章中又在應(yīng)用 React Fiber 的場景下,重復(fù)剛才的例子,不會再出現(xiàn)頁面卡頓,交互自然而順暢。
瀏覽器主線程的 performance 如下圖所示:
可以看到:在黃色 JavaScript 執(zhí)行過程中,也就是 React 占用瀏覽器主線程期間,瀏覽器在也在重新計算 DOM Tree,并且進行重繪。只管來看,黃色和紫色等互相交替,同時頁面截圖顯示,用戶輸入得以及時響應(yīng)。簡單說,在 React 占用瀏覽器主線程期間,瀏覽器也在與用戶交互。這顯然是“更好的性能”表現(xiàn)。
以上是 React 應(yīng)用第一種方法:“將耗時高的任務(wù)分段”,達到了性能突破。下面我們再來看另一種“民間”做法,應(yīng)用 Web worker。
React 結(jié)合 Web worker關(guān)于 Web worker 的概念此文不再贅述,大家可以訪問 MDN 地址進行了解。我們聚焦思考點:如果讓 React 接入 Web worker 的話,切入點在哪里,該如何實施?
總所周知,標準的 React 應(yīng)用由兩部分構(gòu)成:
React core:負責絕大部分復(fù)雜的 Virtual DOM 計算;
React-Dom:負責與瀏覽器真實 DOM 交互來展示內(nèi)容。
那么答案很簡單,我們嘗試在 Web worker 中運行 React Virtual DOM 的相關(guān)計算。即將 React core 部分移入 Web worker 線程中。
確實有人提出了這樣的想法,請參考 React 倉庫 第 #3092 號 Issue,這也吸引來了 Dan Abramov 的討論。雖然這樣的提案被拒絕,但這并不妨礙我們讓 React 結(jié)合 worker 做試驗。
Talk is cheap, show me the code, and demo:
讀者可以訪問這里,該網(wǎng)站分別用原生 React 和接入 Web worker 版 React 實現(xiàn)了兩個應(yīng)用,并對比其性能表現(xiàn)。關(guān)于代碼部分,感興趣的同學(xué)可以私信我。
最終結(jié)論:只有當大量的節(jié)點發(fā)生變化的時,Web worker 提升渲染性能才會有一些效果。當節(jié)點數(shù)量非常少的時候,接入 Web worker 的性能可能是負收益。我認為這是由于 worker 線程和主線程之間的通信成本所致。
這么看,Web worker 版本的 React 仍有性能提升空間,我簡單總結(jié)如下:
因為 worker 線程和主線程在使用 postMessage 通信時,性能成本較大,我們可以采用 batching 思想減少通信的次數(shù)。
如果在每次 DOM 需要改變時,都調(diào)用 postMessage 通知主線程,不是特別明智。所以可以用 batching 思想,將 worker 線程中計算出來的 DOM 待更新內(nèi)容進行收集,再統(tǒng)一發(fā)送。這樣一來,batching 的粒度就很有意思了。如果我們走極端,每次 batching 收集的變更都非常多,遲遲不向主線程發(fā)送,那么在一次 batching 時就給瀏覽器真正的渲染過程帶來了壓力,反而適得其反。
使用 postMessage 傳遞消息時,采用 transferable objects 進行數(shù)據(jù)負載
關(guān)于 worker 版 syntheticEvent
原生 React 有一套事件系統(tǒng),它在最頂層監(jiān)聽所有的瀏覽器事件,之后將它們轉(zhuǎn)化為合成事件(syntheticEvent),傳遞給我們在 Virtual DOM 上定義的事件監(jiān)聽者。
對于我們的 Web worker,由于 worker 線程不能直接操作 DOM,也就不能監(jiān)聽瀏覽器事件。因此所有事件同樣都在主線程中處理,轉(zhuǎn)化為虛擬事件再傳遞給 worker 線程進行發(fā)布,也就意味著所有關(guān)于創(chuàng)建虛擬事件的操作還是都在主線程中進行,一個可能改善的方案可以考慮直接將原始事件傳遞給 worker,由 worker 來生成模擬事件并冒泡傳遞。
關(guān)于 React 結(jié)合 worker 還有很多值得深挖的內(nèi)容,比如:事件處理方面 preventDefault 和 stopPropogation 的同步性保障(worker 線程和主線程通信是異步的);使用 multiple worker(一個以上 worker)進行探究等。如果讀者有興趣,我會專門寫篇文章介紹。
Redux 和 Web worker既然 React 可以接入 Web worker,那么 Redux 當然也能借鑒這樣的思想,將 Redux 中 reducer 復(fù)雜的純計算過程放在 worker 線程里,是不是一個很好的思路?
我使用 “N-皇后問題” 模擬大型計算,除了這個極其耗時的算法,頁面中還運行這么幾個模塊,來實現(xiàn)頻繁更新 DOM 的渲染邏輯:
一個實時每 16 毫秒,顯示計數(shù)(每秒增加 1)的 blinker 模塊;
一個定時每 500 毫秒,更新背景顏色的 counter 模塊;
一個永久往復(fù)運動的 slider 模塊;
一個每 16 毫秒翻轉(zhuǎn) 5 度的 spinner 模塊
如圖:
這些模塊都定時頻繁地更新 DOM 樣式,進行渲染。正常情況下,在 JavaScript 主線程進行 N-皇后計算時,這些渲染過程都將被卡頓。
如果將 N-皇后計算放置到 worker 線程,我們會發(fā)現(xiàn) demo 展現(xiàn)了令人驚訝的性能提升,完全絲滑毫無卡頓。如上圖,左半部分為正常版本,不出意外出現(xiàn)了頁面卡頓,右側(cè)是接入 worker 之后的應(yīng)用。
在實現(xiàn)層面,借助 Redux 庫的 enchancer 設(shè)計,完成了抽象封裝。
一個 store enhancer,實際上就是一個 curry 化的高階函數(shù),這和 React 中的高階組件的概念很相似,同時也類似我們更加熟悉的中間件。其實參考 Redux 源碼,會發(fā)現(xiàn) Redux 源碼中 applyMiddleware 方法的執(zhí)行結(jié)果就是一個 store enhancer。
那么為什么不選擇中間件,而是使用 enhancer 來實現(xiàn)呢?這個 Redux worker demo 所采用的公共庫設(shè)計思路非常有趣,關(guān)于神奇的 Redux 高階內(nèi)容不再展開,感興趣的讀者可以在我新出版的書中找到相應(yīng)內(nèi)容。這也就到了廣告時間。。。
《React 狀態(tài)管理與同構(gòu)實戰(zhàn)》這本書由我和前端知名技術(shù)大佬顏海鏡合力打磨,凝結(jié)了我們在學(xué)習(xí)、實踐 React 框架過程中的積累和心得。除了 React 框架使用介紹以外,著重剖析了狀態(tài)管理以及服務(wù)端渲染同構(gòu)應(yīng)用方面的內(nèi)容。同時吸取了社區(qū)大量優(yōu)秀思想,進行歸納比對。
本書受到百度公司副總裁沈抖、百度資深前端工程師董睿,以及知名 JavaScript 語言專家阮一峰、Node.js 布道者狼叔、Flarum 中文社區(qū)創(chuàng)始人 justjavac、新浪移動前端技術(shù)專家小爝、百度資深前端工程師顧軼靈等前端圈眾多專家大咖的聯(lián)合力薦。
有興趣的讀者可以點擊這里,了解詳情。也可以掃描下面的二維碼購買。再次感謝各位的支持與鼓勵!懇請各位批評指正!
最后,前端學(xué)習(xí)永無止境,希望和每一位技術(shù)愛好者共同進步,大家可以在知乎找到我!
Happy coding!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/108160.html
摘要:分析性能的影響但是需要注意時間單位,只是微秒而已,毫秒的千分之一秒的百萬分之一。在這種情況下,優(yōu)化毫秒的性能隱患無異于撿了芝麻丟了西瓜。 同步自:https://sulin.me/2019/T2ZXZB.... 在分布式系統(tǒng)開發(fā)中,我們經(jīng)常需要將各種各樣的狀態(tài)碼、錯誤信息傳遞給最外層的調(diào)用方,這個調(diào)用方通常是http/api接口,錯誤信息比如登錄失效、參數(shù)錯誤等等。 最外層接口暴露的...
摘要:的前生今世系統(tǒng)系統(tǒng)作為全球第一大系統(tǒng),基于開發(fā)的移動端有著諸多的性能優(yōu)勢。官方提供了豐富的原生接口封裝系統(tǒng)結(jié)構(gòu)圖像處理引擎年圖像處理引擎成立,用來展示火狐和其他自家的產(chǎn)品使用。而語言早已突破階段,正穩(wěn)步邁向階段。 showImg(https://segmentfault.com/img/remote/1460000018724305); Android 的前生今世 Android 系統(tǒng)...
閱讀 3311·2021-11-24 10:43
閱讀 4283·2021-11-24 10:33
閱讀 3860·2021-11-22 09:34
閱讀 2186·2021-10-11 10:58
閱讀 3837·2021-10-11 10:58
閱讀 922·2021-09-27 13:36
閱讀 3650·2019-08-30 15:54
閱讀 3029·2019-08-29 18:41