不知不覺,春節就過完了,還沒來得及好好享受就沒了。好想來一場說走就走的旅行✈️,不吹水了,直接進入正題。
最近在做一個需求,發現了薄弱的地方,趁這個好機會深入了解一下,拓寬一下視野~
眾所周知,網頁不僅應該被快速加載,同時還應該流暢運行,比如快速響應的交互,如絲般順滑的動畫……
在實際開發中如何做到上面所說的效果呢?
第一個是 首屏呈現時間,網上的資料已經非常非常多了,壓縮代碼,使用webp圖片,使用sprite,按需加載,“直出”,CDN……
第二個是 16ms 優化,本篇重點講16ms的優化。
一. 瀏覽器渲染原理介紹
大多數設備的刷新頻率是60次/秒,(1000/60 = 16.6ms)也就說是瀏覽器對每一幀畫面的渲染工作要在16ms內完成,超出這個時間,頁面的渲染就會出現卡頓現象,影響用戶體驗。
這就是上圖中的<16ms。瀏覽器在一幀里面,會做以下這些動作。 當然,有些步驟(比如 layout,paint)是可以省略的。

如果改變屬性在上面圖中越往左,那么影響就越大,效率就越低。
瀏覽器渲染的流程如下:
- 獲取 DOM 並將其分割為多個層(RenderLayer)
- 將每個層柵格化,並獨立的繪制進位圖中
- 將這些位圖作為紋理上傳至 GPU
- 復合多個層來生成最終的屏幕圖像(終極layer)。
從上面圖中可以看出,如果只是改變composite(渲染層合並),那效率就會大大提高。
下面粗略地列出改變哪些樣式會分別改變渲染過程的哪一模塊。

從上圖可以看到 transform,opacity 只會改變composite(渲染層合並),為什么呢?因為開啟了GPU加速。
開啟 GPU 加速
小tips:先選中timeline的某一幀,然后選擇下面的layer標簽tab,可以左右拖動該模塊出現3d
我們可以看到頁面上由如下層組成:

雖然我們最終在瀏覽器上看到的只是一個復印版,即最終只有一個層。類似於PhotoShop軟件中的“圖層”概念,最后合並所有可視圖層,輸出一張圖片到屏幕上
但是實際上一個頁面會因為一些規則被分成相應的層,一旦被獨立出來之后,便不會再影響其他dom的布局,因為它改變之后,只是“貼上”了頁面。
- 3D 或透視變換(perspective transform) CSS 屬性
- 使用加速視頻解碼的
<video>元素 - 擁有 3D (WebGL) 上下文或加速的 2D 上下文的
<canvas>元素 - 混合插件(如 Flash)
- 對自己的 opacity 做 CSS 動畫或使用一個動畫 webkit 變換的元素
- 擁有加速 CSS 過濾器的元素
- 元素有一個包含復合層的后代節點(換句話說,就是一個元素擁有一個子元素,該子元素在自己的層里)
- 元素有一個 z-index 較低且包含一個復合層的兄弟元素(換句話說就是該元素在復合層上面渲染)
- 在webkit內核的瀏覽器中,如果有上述情況,則會創建一個獨立的layer。
說了這么多瀏覽器渲染的原理,如果沒有尺子測量也毫無用處。那么,下面就選尺子去丈量:谷歌開發工具的Timeline。
二. 谷歌開發工具 Timeline 的常用功能
1. 點擊左上角的錄制之后,錄制結束后會生成下面的樣子,紅色區域內就是幀了,移動上去可以看到每一幀的頻率,如果>60fps,就是比較流暢,如果<60fps,就會感到卡頓。

![]()
2. 在timeline下面,可以看到各個模塊的耗時,可以定位到耗時較大的函數上面,對該函數進行優化。



工具也有了,瀏覽器渲染的原理也知道了,接下來是結合實際項目進行優化.
三. 在實際項目中進行 16.6ms 優化
結合上面的渲染流程圖,我們可以針對性的分析並優化下面的一些步驟
- 優化JavaScript的執行效率
- 降低樣式計算的范圍和復雜度
- 避免大規模、復雜的布局
- 簡化繪制的復雜度、減少繪制區域
- 優先使用渲染層合並屬性、控制層數量
- 對用戶輸入事件的處理函數去抖動(移動設備)
1. 讀寫分離,批量操作
JavaScript腳本運行的時候,它能獲取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。
因此,如果你在當前幀獲取屬性之前又對元素節點有改動,那就會導致瀏覽器必須先應用屬性修改,結果執行布局過程,最后再執行JavaScript邏輯。
// 先寫后讀,觸發強制布局
function logBoxHeight() {
// 更新box樣式
box.classList.add('super-big'); // 為了返回box的offersetHeight值 // 瀏覽器必須先應用屬性修改,接着執行布局過程 console.log(box.offsetHeight); }
// 先讀后寫,避免強制布局 function logBoxHeight() { // 獲取box.offsetHeight console.log(box.offsetHeight); // 更新box樣式 box.classList.add('super-big'); }
2. 閉包緩存計算結果 (需要頻繁的調用,計算的函數)
1 getMaxWidth: (function () {
2 var cache = {}; 3 function getwidth() { 4 if (maxWidth in cache) { 5 return cache[maxWidth]; 6 } 7 var target = this.node, 8 width = this.width, 9 screen = document.body.clientWidth, 10 num = target.length, 11 maxWidth = num * width + 10 * num + 20 - screen; 12 cache[maxWidth] = maxWidth; 13 return maxWidth; 14 } 15 return getwidth; 16 })(),
改成這種方式后,直接蹭蹭蹭~ 減少了10多ms
然后在調用requestAnimationFrame的時候,如果你在一開始就做了讀取樣式屬性的操作,那么將會觸發瀏覽器的強制同步布局操作(即在javascript階段中執行布局),這樣會導致多次布局,效率低下。
優化如下:
window.requestAnimationFrame(function () { context.animateTo(nowPos); //需要更新位置的交給RAF });
續上面,開啟paint flashing 之后,可以看到瀏覽器重新繪制了哪些區域。發現有一些不必要重繪的區域也重繪了~給這些開啟GPU優化(上文中提到)
直接看 timeline 效果,全綠了~懸着的心終於放下了


參考文章: http://www.jianshu.com/p/a32b890c29b1
