使用 Chrome Timeline 來優化頁面性能

本文來自 Coding 博客:blog.coding.net/blog/Ch

有時候,我們就是會不由自主地寫出一些低效的代碼,嚴重影響頁面運行的效率。或者我們接手的項目中,前人寫出來的代碼千奇百怪,比如為了一個 Canvas 特效需要同時繪製 600 個三角形,又比如 Coding.net 的任務中心需要同時 watch 上萬個變數的變化等等。那麼,如果我們遇到了一個比較低效的頁面,應該如何去優化它呢?

優化前的準備:知己知彼

在一切開始之前,我們先打開 F12 面板,熟悉一下我們接下來要用到的工具:Timeline:

嗯沒錯就是它。下面逐一介紹一下吧。區域 1 是一個縮略圖,可以看到除了時間軸以外被上下分成了四塊,分別代表 FPS、CPU 時間、網路通信時間、堆棧佔用;這個縮略圖可以橫向縮放,白色區域是下面可以看到的時間段(灰色當然是不可見的啦)。區域 2 可以看一些交互事件,例如你滾動了一下頁面,那麼這裡會出現一個 scroll 的線段,線段覆蓋的範圍就是滾動經過的時間。區域 3 則是具體的事件列表了。

一開始沒有記錄的時候,所有的區域都是空的。開始統計和結束統計都很簡單,左上角那坨黑色的圓圈就是。它右邊那個長得像「禁止通行」的按鈕是用來清除現有記錄的。當有數據的時候,我們把滑鼠滾輪向上滾,可以看到區域被放大了:

短短的時間裡,瀏覽器做了這麼多事情。對於一般的屏幕,原則上來說一秒要往屏幕上繪製 60 幀,所以理論上講我們一幀內的計算時間不能超過 16 毫秒,然而瀏覽器除了執行我們的代碼以外,還要干點別的(例如計算 CSS,播放音頻……),所以其實我們能用的只有 10~12 毫秒左右。

差不多熟悉操作了,那麼就來一下實戰吧!假如有一天,你接手了這樣一段代碼:

<!-- 一段小動畫:點擊按鈕之後會有一個爆炸的粒子效果 -->n<!DOCTYPE html>n<html>n<head>n <meta charset="utf-8">n <title>Test</title>n <style>n .main {n position: relative;n width: 500px;n height: 500px;n background: #000;n overflow: hidden;n }n .circle {n position: absolute;n border-radius: 50%;n border: 1px solid #FFF;n width: 8px;n height: 8px;n }n </style>n</head>n<body>n <div class="main"></div>n <hr>n <button onclick="showAnimation()">點我</button>n <script src="jquery.min.js"></script>n <script src="animation.js"></script>n</body>n</html>n

// animation.jsnn// 粒子總數nvar COUNT = 500;n// 重力nvar G = -0.1;n// 摩擦力nvar F = -0.04;nnfunction init() {n for (var i = 0; i < COUNT; i++) {n var d = Math.random() * 2 * Math.PI;n var v = Math.random() * 5;n var circle = $(<div id="circle- + i + " class="circle" data-x="250" data-y="250" data-d=" + d + " data-v=" + v + "></div>);n circle.appendTo($(.main));n }n}nnfunction updateCircle() {n for (var i = 0; i < COUNT; i++) {n var x = parseFloat($(#circle- + i).attr(data-x));n var y = parseFloat($(#circle- + i).attr(data-y));n var d = parseFloat($(#circle- + i).attr(data-d));n var v = parseFloat($(#circle- + i).attr(data-v));n var vx = v * Math.cos(d);n var vy = v * Math.sin(d);n if (Math.abs(vx) < 1e-9) vx = 0;n // 速度分量改變n vx += F * Math.cos(d);n vy += F * Math.sin(d) + G;n // 計算新速度n v = Math.sqrt(vx * vx + vy * vy);n if (vy > 0) d = Math.acos(vx / v);n else d = -Math.acos(vx / v);n // 位移分量改變n x += vx;n y += vy;n $(#circle- + i).attr(data-x, x);n $(#circle- + i).attr(data-y, y);n $(#circle- + i).attr(data-d, d);n $(#circle- + i).attr(data-v, v);n $(#circle- + i).css({top: 400 - y, left: x});n }n}nnvar interval = null;nnfunction showAnimation() {n if (interval) clearInterval(interval);n $(.main).html();n init();n interval = setInterval(updateCircle, 1000 / 60);n}n

效果如下(右上角的 FPS 計數器是 Chrome 調試工具自帶的):

只有 10 FPS……10 FPS……坑爹呢這是!

好吧,打開 Timeline,按下記錄按鈕,點一下頁面中的「點我」,稍微過一會兒停止記錄,就會得到一些數據。放大一些,對 jQuery 比較熟悉的同學可以看出來,這些大部分是 jQuery 的函數。我們點一下那個 updateCircle 的區塊,然後看下面:

這裡告訴我們,這個函數運行了多久、函數代碼在哪兒。我們點一下那個鏈接,於是就跳到了 Source 頁:

是不是很震撼,之前這個頁面只是用來 Debug 的,沒想到現在居然帶了精確到行的運行時間統計。當然,這個時間是當前這一行在「剛才我們點擊的區塊對應的執行時間段」中運行的時間。所以我們就拿最慢的幾句話來下手吧!

優化一:減少 DOM 操作

看到這幾行代碼,第一反應是:mdzz。本來 DOM 操作就慢,還要在字元串和 float 之間轉來轉去。果斷改掉!於是用一個單獨的數組來存 x、y、d、v 這些屬性。

var objects = [];n// 在 init 函數中nobjects.push({n x: 250,n y: 250,n d: d,n v: vn});n// 在 updateCircle 函數中nvar x = objects[i].x;nvar y = objects[i].y;nvar d = objects[i].d;nvar v = objects[i].v;n// ….nobjects[i].x = x;nobjects[i].y = y;nobjects[i].d = d;nobjects[i].v = v;n

效果顯著!我們再來看一下精確到行的數據:

優化二:減少不必要的運算

所以最耗時的那句話已經變成了計算 vx 和 vy,畢竟三角函數演算法比較複雜嘛,可以理解。至於後面的三角函數為什麼那麼快,我猜可能是 Chrome 的 V8 引擎將其緩存了(這句話不保證正確性)。然而不知道大家有沒有發現,其實計算 d 完全沒必要!我們只需要存 vx 和 vy 即可,不需要存 v 和 d!

// initnvar vx = v * Math.cos(d);nvar vy = v * Math.sin(d);nobjects.push({n x: 250,n y: 250,n vx: vx,n vy: vyn});n// updateCirclenvar vx = objects[i].vx;nvar vy = objects[i].vy;n// 計算新速度nvar v = Math.sqrt(vx * vx + vy * vy);nif (Math.abs(vx) < 1e-9) vx = 0;n// 速度分量改變nvx += F * vx / v;nvy += F * vy / v + G;n// ….nobjects[i].vx = vx;nobjects[i].vy = vy;n

只有加減乘除和開平方運算,每次比原來的時間又少了兩毫秒。從流暢的角度來說其實已經可以滿幀運行了,然而為什麼我還是覺得偶爾會有點卡呢?

優化三:替換 setInterval

既然偶爾會掉幀,那麼就看看是怎麼掉的唄~原則上來說,在每一次瀏覽器進行繪製之前,Timeline 裡面應該有一個叫 Paint 的事件,就像這樣:

看到這些綠色的東西了沒?就是它們!看上面的時間軸,雖然代碼中 setInterval 的長度是 1000/16 毫秒,但是其實根本不能保證!所以我們需要使用 requestAnimationFrame 來代替它。這是瀏覽器自帶的專門為動畫服務的函數,瀏覽器會自動優化這個函數的調用時機。並且如果頁面被隱藏,瀏覽器還會自動暫停調用,有效地減少了 CPU 的開銷。

// 在 updateCircle 最後加一句nrequestAnimationFrame(updateCircle);n// 去掉全部跟 setInterval 有關的句子,把 showAnimation 最後一句直接改成這個nupdateCircle();n

我們至少可以保證,我們每算一次,屏幕上就會顯示一次,因此不會掉幀(前提是每計算一次的時間小於 12ms)。但是雖然計算時間少了,瀏覽器重計算樣式、繪製圖像的時間可是一點都沒變。能不能再做優化呢?

優化四:使用硬體加速、避免反覆查找元素

如果我們用 transform 來代替 left 和 top 來對元素進行定位,那麼瀏覽器會為這個元素單獨創立一個合成層,專門使用 GPU 進行渲染,這樣可以把重計算的代價降到最低。有興趣的同學可以研究一下「CSS 硬體加速」的機制。同時,我們可以緩存一下 jQuery 的元素(或者 DOM 元素),這樣不用每次都重新查找,也能稍微提高一點效率。如果把元素緩存在 objects 數組中,那麼連 id 都不用寫了!

// initnvar circle = $(<div class="circle"></div>);nobjects.push({n x: 250,n y: 250,n vx: vx,n vy: vy,n // 其實可以只存 DOM,不存 jQuery 對象n circle: circle[0]n});n// updateCircle 裡面 for 循環的最後一句話替換掉nobjects[i].circle.style.transform = translate( + x + px, + (400 - y) + px);n

看起來是不是很爽了?

其實,優化是無止境的,例如我在 init 函數中完全可以不用 jQuery,改用 createDocumentFragment 來拼接元素,這樣初始化的時間就可以急劇縮短;調換 updateCircle 中的幾個語句的順序,在 V8 引擎下效率可能會有一定的提升;甚至還可以結合 Profile 面板來分析內存佔用,查看瀏覽器繪圖的細節……然而個人感覺並用不到這麼極限的優化。對於一個項目來說,如果單純為了優化而寫一些奇怪的代碼,是很不合算的。

P.S. 全部的代碼在這裡,歡迎吐槽:

未優化版 | 優化版


推薦閱讀:

極樂技術周報(第三十期)
前端異常監控平台的設計思路及實現
精讀《null >= 0?》
【Web系列】小說在線閱讀

TAG:GoogleChrome | 前端开发 | Coding |