摘要:接下來看下偽代碼調(diào)度算法偽代碼原來這段寫的匆忙且不好,重新更新了一篇講調(diào)度算法的大概實(shí)現(xiàn)性能改善的原理二。
問題背景
React16 更新了底層架構(gòu),新架構(gòu)主要解決更新節(jié)點(diǎn)過多時(shí),頁碼卡頓的問題。譬如如下代碼,根據(jù)用戶輸入的文字生成10000行數(shù)據(jù),用戶輸入框會(huì)出現(xiàn)卡頓現(xiàn)象。
class App extends React.Component { constructor( props ) { super( props ); this.state = { rowData: [] } } handleUserInput = (e)=>{ let userInput = e.target.value; let newRowData = []; for( let i = 0; i < 10000; i++) { newRowData.push( userInput ); } this.setState( { rowData: newRowData } ) } renderRows() { return this.rowData.map( (s,index)=>{ return (卡頓的原因 FPS) } ) } render() { return ( {s} ); } }{ this.renderRows() }
為了引出瀏覽器卡頓真正的原因,我們先簡單介紹一個(gè)概念:FPS(Frames Per Second) - 每秒傳輸幀數(shù)。舉個(gè)例子,一般來說動(dòng)畫片是如何動(dòng)起來的呢?是以極快的速度連續(xù)播放靜態(tài)的圖片,利用視網(wǎng)膜圖像殘留效應(yīng),讓人產(chǎn)生動(dòng)起來的錯(cuò)覺。那么這個(gè)播放要多塊呢?每秒最少要展示24張圖片,觀眾才勉強(qiáng)不會(huì)感受到畫面延時(shí)(即 FPS 達(dá)到24,不會(huì)讓人覺得卡頓)。
頁面繪制過程瀏覽器其實(shí)也是類似的原理,每間隔一定的時(shí)間重新繪制一下當(dāng)前頁面。一般來說這個(gè)頻率是每秒60次。也就是說每16毫秒( 1 / 60 ≈ 0.0167 )瀏覽器會(huì)有一個(gè)周期性地重繪行為,這每16毫秒我們稱為一幀。這一幀的時(shí)間里面瀏覽器做些什么事情呢:
執(zhí)行JS。
計(jì)算Style。
構(gòu)建布局模型(Layout)。
繪制圖層樣式(Paint)。
組合計(jì)算渲染呈現(xiàn)結(jié)果(Composite)。
inter-frame idle period.jpg
這個(gè)過程是順序的,如果 JS 執(zhí)行的時(shí)間過長,那么后續(xù)的步驟也就會(huì)被相應(yīng)的延后,導(dǎo)致的后果就是一幀的時(shí)間變長,F(xiàn)PS 變低。人直觀的感受就是頁面變卡頓?;氐缴厦娴睦樱幌伦痈?0000條數(shù)據(jù)導(dǎo)致 React 執(zhí)行了相當(dāng)長的時(shí)間,讓瀏覽器這段時(shí)間內(nèi)無法做其他事情,下一幀被延遲了。
有人會(huì)想到說,誒,一次執(zhí)行時(shí)間太長會(huì)卡我能理解,但是為啥我以前用定時(shí)器做 JS 動(dòng)畫有時(shí)也會(huì)卡呢?下面我們就分析下原因。
setTimeout/setInterval我們把 setTimeout 和瀏覽器幀流兩條時(shí)間線放在一起看一下( 綠色是 paint,紫色是 render,黃色是執(zhí)行 JS ):
第一種完美的情況,就是 setTimeout 執(zhí)行的頻率和瀏覽器的幀率相同。
timeline-perfect-frequency.png
太頻繁,導(dǎo)致每一幀的元素變化過大(不是每次改變元素的效果都被顯示出來),表現(xiàn)為動(dòng)畫不順滑。譬如,你期望元素每次移動(dòng)10像素,但是按之前的原理,用戶看到的是元素每次移動(dòng)了40像素。
timeline-too-frequent.png
setTimeout 的頻率低于瀏覽器默認(rèn)幀率,導(dǎo)致跳幀,表現(xiàn)也是不順滑。這個(gè)就不用說了,元素可能幾幀才動(dòng)一次。
timeline-skip-frame.png
setTimeout 某次或者每次執(zhí)行的函數(shù)時(shí)間過長,導(dǎo)致瀏覽器的 FPS 降低,表現(xiàn)為動(dòng)畫卡頓。這種別說動(dòng)畫卡,頁面也卡了。
timeline-delay.png
想象一下,當(dāng)你不知道瀏覽器頁面繪制原理的時(shí)候是不是全憑感覺來設(shè)置 setTimeout 的間隔?當(dāng)然你也可以把 setTimeout 的間隔設(shè)置成16毫秒。不過如果對 event loop 機(jī)制了解的話,你會(huì)知道這個(gè)只能大致保證按這個(gè)時(shí)間間隔執(zhí)行,并不會(huì)嚴(yán)格保證。setInterval 也是類似,但是比 setTimeout 更不可控。
解決方案回過頭來我們仔細(xì)分解下每一幀瀏覽器要做些什么(見下圖),先是響應(yīng)各種事件,然后執(zhí)行 event loop 中的任務(wù),然后是一段 raf 時(shí)間,最后是計(jì)算排版(layout)和重新繪制(paint)。大致你可以認(rèn)為是先執(zhí)行程序,然后再根據(jù) JS 執(zhí)行的結(jié)果重繪頁面,當(dāng)然如果 dom 元素沒有任何變化,那么重繪這個(gè)步驟就省了。
life of a frame.png
如果我們能保證 JS 動(dòng)畫的每次執(zhí)行都在重繪前,那么我們就能做到動(dòng)畫的順滑,setTimeout 無法保證,但是瀏覽器提供了新的 API 來幫助我們了。
瀏覽器新APIrequestAnimationFrame
這個(gè)函數(shù)的作用就是告訴瀏覽器你希望執(zhí)行一段 JS,并且要求瀏覽器在下次重繪之前調(diào)用這段 JS 所在的回調(diào)函數(shù)。
requestAnimationFrame( function(){ document.body.style.width = "100px"; } )
上述代碼執(zhí)行后,在瀏覽器繪制頁面的下一幀重繪前,會(huì)執(zhí)行回調(diào)函數(shù),那么就能保證修改的 dom 的效果能在下一幀被顯示出來。回看上面的幀的生命周期,raf 時(shí)間就是留給 requestAnimationFrame 所注冊的回調(diào)函數(shù)執(zhí)行用的。這樣我們把以前的 setTimeout 動(dòng)畫就可以用 requestAnimationFrame 來改造。
// 舊版:讓元素右移500像素 function moveToRight( div ) { let left = parseInt( div.style.left ); if ( left < 500 ) { div.style.left = (left+10+"px"); setTimeout( function(){ moveToRight( div ); }, 16 ) } else { return; } } moveToRight( div ); // 新版:讓元素右移500像素 function moveToRight( div ) { let left = parseInt( div.style.left ); if ( left < 500 ) { div.style.left = (left+10+"px"); requestAnimationFrame( function(){ moveToRight( div ); } ) } else { return; } } requestAnimationFrame( function(){ moveToRight( div ); } )
特別注意:不是用了 requestAnimationFrame 后動(dòng)畫就流暢了。如果你傳入 requestAnimationFrame 的回調(diào)函數(shù)執(zhí)行的 JS 耗時(shí)過長,一樣會(huì)導(dǎo)致后續(xù)步驟的延時(shí),引起瀏覽器 FPS 的下降。所以這點(diǎn)在寫代碼的時(shí)候要注意。
現(xiàn)在有一個(gè)問題,傳入 requestAnimationFrame 的回調(diào)函數(shù)一定是會(huì)被被安排在下一次重繪前所調(diào)用的,但是如果 raf 時(shí)間之前就已經(jīng)執(zhí)行了長時(shí)間的 JS,那么我再執(zhí)行這個(gè)回調(diào)豈不是雪上加霜?我能不能要求這種情況說,我的代碼也不是很緊急,判斷下如果當(dāng)前幀不“忙”,我就執(zhí)行,如果幀“忙”,我可以等下一幀之類的呢?好!下一個(gè) API 來了。
requestIdleCallback
這個(gè)函數(shù)告訴瀏覽器,在空閑時(shí)期依次執(zhí)行注冊的回調(diào)函數(shù)。什么意思呢?上面我們說過瀏覽器在一幀的時(shí)間里面要做這個(gè)事,那個(gè)事,但是并不是每時(shí)每刻這些事情都耗時(shí)的。譬如你打開頁面后什么都不做,那么一幀16毫秒之內(nèi)又沒有啥 JS 需要執(zhí)行又沒有大量的重繪工作,產(chǎn)生了有很多空余時(shí)間??聪聢D,黃色部分就是一幀內(nèi)的空余時(shí)間,當(dāng)瀏覽器發(fā)現(xiàn)一幀有空余時(shí)間就會(huì)看下有沒有調(diào)用 requestIdleCallback 注冊的回調(diào)函數(shù),有的話就執(zhí)行下。如果執(zhí)行某個(gè)回調(diào)前看到幀結(jié)束了,那么就等下一次有空閑時(shí)間接著執(zhí)行剩余的回調(diào)函數(shù)。
inter-frame idle period.jpg
有了 requestAnimationFrame 和 requestIdleCallback 我們就能比以前更細(xì)粒度的控制 JS 執(zhí)行的時(shí)間了。接下來我們看下基于這個(gè)原理 React 如何優(yōu)化它的更新 dom 的機(jī)制。
React調(diào)度算法React 代碼中如果某處 setState 被調(diào)用引起了一系列更新,React 大致要做的是生成新的虛擬 dom 樹,然后和老的虛擬 dom 樹做比較,生成更新列表,最后根據(jù)這個(gè)列表更新真實(shí)的 dom。當(dāng)然更新 dom 耗時(shí)在 JS 層面現(xiàn)階段是沒法優(yōu)化了,而生成虛擬 dom,做新老虛擬 dom 比較過程的耗時(shí),是可能隨著應(yīng)用的復(fù)雜程度而增加的。React16 之前絕大多數(shù)情況是一次完成虛擬 dom 到真實(shí) dom 更新的整個(gè)過程的。那么這個(gè)過程如果在一幀里面耗時(shí)過長,頁面就卡頓了。React16 的思路就是想利用 requestAnimationFrame 和 requestIdleCallback 兩個(gè)新 API,把一次耗時(shí)較長的更新任務(wù)分解到多個(gè)幀去執(zhí)行。這樣給瀏覽器留出時(shí)間去響應(yīng)頁面上的其他事件,解決卡頓的問題。接下來看下偽代碼:
調(diào)度算法偽代碼原來這段寫的匆忙且不好,重新更新了一篇講調(diào)度算法的大概實(shí)現(xiàn)React16性能改善的原理(二)。
原更新步驟大致為
// 原更新步驟大致為: setState( partialState ) { var inst = this._instance; var nextState = Object.assign( {}, inst.state, partialState ); // 根據(jù)新的 state 生成新的虛擬 dom inst.state = nextState; var nextRenderedElement = inst.render(); // 獲取上一次的虛擬 dom var prevComponentInstance = this._renderedComponent; // render 中的根節(jié)點(diǎn)的渲染對象 var prevRenderedElement = prevComponentInstance._currentElement; if( shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement) ) { // 更新 dom node prevComponentInstance.receiveComponent( nextRenderedElement ) } }
根據(jù)新的優(yōu)化思路,React16新的更新過長大致為:
setState( partialState ) { updateQueue.push( { instance: this, partialState: partialState } ); requestIdleCallback( doDiff ) } function doDiff( deadline ) { let nextUpdate = updateQueue.shift(); let pendingCommit = []; // 如果更新隊(duì)列里面有更新,且時(shí)間富裕,則逐步計(jì)算出需要更新的內(nèi)容 while( nextUpdate && deadline.timeRemaining()>ENOUGH_TIME ) { // 生成 fiber 節(jié)點(diǎn),對比新老節(jié)點(diǎn),生成更新dom的任務(wù) pendingCommit.push( calculateDomModification(nextUpdate) ); // 把更新 dom 的任務(wù)加入待更新隊(duì)列 nextUpdate = updateQueue.shift(); } // 一次把當(dāng)前時(shí)間片所有的 diff 出的更新任務(wù)都更新到 dom 上 if ( pendingCommit.lengt>0 ) { commitAllWork( pendingCommit ); } // 如果更新隊(duì)列還有更新,但是時(shí)間片耗盡了,那么在下次空閑時(shí)間再更新 if ( nextUnitOfWork || updateQueue.length > 0 ) { requestIdleCallback( doDiff ); } }
實(shí)際代碼當(dāng)然要比這個(gè)復(fù)雜的多,React 對上述調(diào)度的實(shí)現(xiàn)基于現(xiàn)實(shí)的考慮進(jìn)行了優(yōu)化:考慮到 1.有的更新是比較緊急的不能等空閑去完成要用 requestAnimationFrame、2.有的是可以放到空閑時(shí)間去執(zhí)行的、3.對于兩個(gè)新 API 的瀏覽器支持不是很好、4.瀏覽器默認(rèn)刷新頻率的的時(shí)間片太短。React 團(tuán)隊(duì)實(shí)現(xiàn)了一個(gè)自己的調(diào)度函數(shù) requestAnimationFrameWithTimeout。
后續(xù)還打算更新其他細(xì)節(jié)的內(nèi)容,等研究好了再更新,譬如:1. 更新任務(wù)不是同步完成的,如果同一個(gè)節(jié)點(diǎn)在還沒有把更新真正反應(yīng)到 dom 上的時(shí)候,有來了一次 setState 怎么辦?
2. React fiber 為什么是鏈?zhǔn)浇Y(jié)構(gòu)?
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/101918.html
摘要:接下來我們就是正式的工作了,用循環(huán)從某個(gè)節(jié)點(diǎn)開始遍歷樹。最后一步判斷全局變量是否存在,如果存在則把這次遍歷樹產(chǎn)生的所有更新一次更新到真實(shí)的上去。 前情提要 上一篇我們提到如果 setState 之后,虛擬 dom diff 比較耗時(shí),那么導(dǎo)致瀏覽器 FPS 降低,使得用戶覺得頁面卡頓。那么 react 新的調(diào)度算法就是把原本一次 diff 的過程切分到各個(gè)幀去執(zhí)行,使得瀏覽器在 dif...
摘要:打包分析與性能優(yōu)化背景在去年年末參與的一個(gè)項(xiàng)目中,項(xiàng)目技術(shù)棧使用,生產(chǎn)環(huán)境全量構(gòu)建將近三分鐘,項(xiàng)目業(yè)務(wù)模塊多達(dá)數(shù)百個(gè),項(xiàng)目依賴數(shù)千個(gè),并且該項(xiàng)目協(xié)同前后端開發(fā)人員較多,提高構(gòu)建效率,成為了改善團(tuán)隊(duì)開發(fā)效率的關(guān)鍵之一。 webpack打包分析與性能優(yōu)化 背景 在去年年末參與的一個(gè)項(xiàng)目中,項(xiàng)目技術(shù)棧使用react+es6+ant-design+webpack+babel,生產(chǎn)環(huán)境全量構(gòu)建將...
摘要:本周在支持機(jī)票的項(xiàng)目中對做了大量改進(jìn),包括性能上與結(jié)構(gòu)上的改進(jìn)。但通過一些簡化改改良,代碼的可靠性大大提高了。此外,還有周邊的優(yōu)化在目錄下提供一個(gè),用于在舊式中替換。改善,里面內(nèi)置了一個(gè)補(bǔ)丁,也是用于改善性能,或中的性能好差。 本周在支持機(jī)票的項(xiàng)目中對anujs做了大量改進(jìn),包括性能上與結(jié)構(gòu)上的改進(jìn)。與1.1.3一樣,還是差一個(gè)組件就完全兼容阿里的antd UI庫。 框架本身的改進(jìn)有:...
摘要:譯文地址譯唯快不破應(yīng)用的個(gè)優(yōu)化步驟前端的逆襲知乎專欄原文地址時(shí)過境遷,應(yīng)用比以往任何時(shí)候都更具交互性。使用負(fù)載均衡方案我們在之前討論緩存的時(shí)候簡要提到了內(nèi)容分發(fā)網(wǎng)絡(luò)。換句話說,元素的串形訪問會(huì)削弱負(fù)載均衡器以最佳形式 歡迎關(guān)注知乎專欄 —— 前端的逆襲歡迎關(guān)注我的博客,知乎,GitHub。 譯文地址:【譯】唯快不破:Web 應(yīng)用的 13 個(gè)優(yōu)化步驟 - 前端的逆襲 - 知乎專欄原文地...
摘要:這次更新主要是改善了對焦點(diǎn)的處理及的語法糖的支持優(yōu)化的性能,將原方法內(nèi)部用到函數(shù)與對象提到全局上來,這就比官方的對象池技術(shù)更能提升性能。 anu1.2.1這次更新主要是改善了對焦點(diǎn)的處理及react16.2的Fragment語法糖的支持 優(yōu)化fiberizeChildren的性能,將原方法內(nèi)部用到函數(shù)與對象提到全局上來,這就比官方的對象池技術(shù)更能提升性能。 修復(fù)受控組件在textar...
閱讀 3141·2021-10-15 09:41
閱讀 3202·2021-09-22 16:05
閱讀 2441·2021-09-22 15:19
閱讀 2904·2021-09-02 15:11
閱讀 2475·2019-08-30 15:52
閱讀 870·2019-08-30 11:06
閱讀 1029·2019-08-29 16:44
閱讀 1303·2019-08-23 18:18