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