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