Effective前端6:避免頁面卡頓


什么是頁面卡頓?如下:

當拖動頁面或者滾動的時候頁面一卡一卡的,看起來不連貫,我們就說頁面卡了,這是一種非常不友好的體驗,怎么衡量頁面卡頓的情況呢?

1. 失幀和幀率FPS

如果你家里買了電視盒的話,在設置里面應該會有一個輸出設置:

上面選中的60Hz就是幀率(frame per second),即一秒鍾60幀,換句話說,一秒鍾的動畫是由60幅靜態圖片連在一起形成的。60fps是動畫播放比較理想、比較基礎的要求。當然如果你的顯卡要是連這個都支持不了的話那就沒辦法了。windows系統有個刷新頻率也是這個意思。

所以卡了,就是失幀了,或者掉幀了,1秒鍾沒有60個畫面,看起來不連貫了。這可能是因為在渲染某些幀所花的時間比較長,導致停留在這些幀的時間較長,所以畫面停頓了。

2. 渲染流程

60fps就要求1幀的時間為1s / 60 = 16.67ms。瀏覽器顯示頁面的時候,要處理js邏輯,還要做渲染,每個執行片段不能超過16.67ms。實際上,瀏覽器內核自身支撐體系運行也需要消耗一些時間,所以留給我們的時間差不多只有10ms。這10ms里面需要做一些什么事情?在Chrome的開發者文檔Rendering Performance里面提到這個流程:

首先你用js做了些邏輯,還觸發了樣式變化,style把應用的樣式規則計算好之后,把影響到的頁面元素進行重新布局,叫做layout,再把它畫到內存的一個畫布里面,paint成了像素,最后把這個畫布刷到屏幕上去,叫做composite,形成一幀。

這幾項的任何一項如果執行時間太長了,就會導致渲染這一幀的時間太長,平均幀率就會掉。假設這一幀花了50ms,那么此時的幀率就為1s / 50ms = 20fps.

當然上面的過程並不一定每一步都會執行,例如:

  1. 你的js只是做一些運算,並沒有增刪DOM或改變CSS,那么后續幾步就不會執行
  2. style只改了顏色等不需要重新layout的屬性就不用執行layout這一步
  3. style改了transform屬性,在blink和edge瀏覽器里面不需要layout和paint,如下面css trigger的說明:

發生掉幀的時候,我們可以使用的Chrome的devtools的timeline來觀察這個過程。以最開始的例子做說明。

3. 掉幀分析

打開timeline的標簽,勾上js profile和paint這兩個選項,然后點擊左邊的記錄按鈕:

在頁面拖動地圖,出現卡頓的情況后,點擊關閉記錄按鈕,就會生成這次操作的詳細過程,先看最上面的overview圖:

 

最上面一欄是幀率,頂點表示60fps,紅色方格表示渲染時間比較長的幀,Chrome把這種情況叫做jank。可以看到上面有3個比較大的低谷,這並不是異常的失幀,這是Chrome檢測到頁面沒有動了,idle空閑了,自動降低幀率。第二欄是CPU,黃色的為script,紫色的是CSS,藍色是html,可以看到往往script占了比較高的CPU。關於timeline更詳細的說明,可以查看chrome的文檔

我們注意到在6s和8s中間CPU占用有一個比較大的峰值,並且失幀得比較厲害:

選中這段區域,進行放大查看:

可以看到有好幾幀都超過了16.67ms,其中有一幀甚至達到了81.8ms,所以難怪卡得那么厲害。我們重點看一下這一幀里面發生了什么。

這一幀的FPS只有1s / 81.8ms = 12fps,點擊第二個tab展開:

其中js的處理用掉了46.8ms(js里面還要更新dom),排第二的rendering花掉了22.9ms,這個rendering包括上面說的css計算和layout:

最后的Painting,時間還是比較少的,只花了2.5ms:

所以最長的開銷是js腳本,並且很可能js里面做了很多dom操作或者改了很多css,導致Rendering的時間也很長。

由於在開始記錄之前勾選了js profile的選項,所以可觀察這些js執行的具體開銷,包括調用的函數棧及每個函數的執行時間:

最上面那個函數是XHR Ready State Change觸發的,也就是說這一整段代碼都是在一個ajax的success回調函數里面執行的。再往下可以看到回調函數里面調用的最耗時的兩個函數:

其一的showMapResut就花費了22.65ms,它又調了removeOldHouses和addNewHouses,這兩個各自的時間約為11ms。

而另一個showResult的時間更多:

快40ms,它下面的doShowResut和resizeContainer最為耗時。

所以我們找到4個最為耗時的函數。那接下來怎么辦呢?

上面已經提到,每一幀留給我們的時間只有10ms。所以可以考慮把上面那4個函數拆了,分別在4個連續的幀里面執行。這樣應該會改善很多。

4. 拆分代碼段

我們把代碼拆成一個個單元,每個單元就是一個task任務,每一幀執行之前就去取一個task執行。並且控制每個task的執行時間都在10ms以內。這樣就可以解決問題。js在渲染每一幀之前會去調requestAnimationFrame(傳一個函數的參數給它去執行)。所以用這一個api,並把task傳給它。我們建立一個任務隊列,為此封裝一個Task類:

使用的時候先new一個Task,然后調draw函數初始化。有任務的時候調addTask插到隊尾,執行任務的時候調shift取出隊頭元素。

上面的實現其實有一點問題,因為requestAnimationFrame是全局的,每次new一個Task,進行draw的時候,會把上一個傳給它的task給覆蓋掉。但是這個是可以從代碼層面上解決的,這里不展開討論。

然后再封裝一個mapTask的單例,存放map頁面的task:

需要插入一個任務的時候就調一下mapTask.add,把上面4個十分耗時的函數分別當作一個任務插進去,下面是原本的執行邏輯:

現在把它改成兩個task,並加到任務隊列里面:

同樣地,把另外兩個也這樣改一下。

然后再拖動地圖,查看效果,會發現頁面瞬間爽滑了好多:

當把頁面拖快的時候還是會有一點卡頓,但是比之前已經好很多。這里還有優化的空間,例如后面兩個函數的執行時間還是比較長,可以把這兩個函數再繼續拆分task。

看一下timeline:

可以看到4個task分別在4幀執行,並且Task3還有很大的優化空間。

除了拆分代碼段的方法外,還有其它一些地方要注意:

5. 其它的優化方法

(1)盡量減少layout

獲取scrollTop、clentWidth等維度屬性時都會觸發layout以獲取實時的值,所以在for循環里面應該把這些值緩存一下。以下代碼:

應該改成:

當循環次數很多的時候,優化版的代碼會明顯提高性能。

獲取一個元素的樣式(getComputedStyle)時,也會觸發layout

另外,能夠使用transform滿足要求的就別使用position/width/height做動畫。

(2)簡化DOM結構

當DOM結構越復雜時,需要重繪的元素也就越多。所以dom應該保持簡單,特別是那些要做動畫的,或者要監聽scroll/mousemove事件的。另外使用flex比使用float在重繪方面會有優勢,詳見:《Avoid Large, Complex Layouts and Layout Thrashing

 

參考:

  1. 淘寶首頁性能優化實踐
  2. 如何評價頁面的性能

擴展閱讀:

  1. Effective前端1:能使用html/css解決的問題就不要使用JS
  2. Effective前端2:優化html標簽
  3. Effective前端3:用CSS畫一個三角形
  4. Effective前端4:盡可能地使用偽元素
  5. Effective前端5:減少前端代碼耦合

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM