網頁性能管理詳解

你遇到過性能不好的網頁嗎?javascript

這種網頁響應很是緩慢,佔用大量的CPU和內存,瀏覽起來經常有卡頓,頁面的動畫效果也不流暢。php

你會有什麼反應?我猜測,大多數用戶會關閉這個頁面,改成訪問其餘網站。做爲一個開發者,確定不肯意看到這種狀況,那麼怎樣才能提升性能呢?css

本文將詳細介紹性能問題的出現緣由,以及解決方法。html

1、網頁生成的過程

要理解網頁性能爲何很差,就要了解網頁是怎麼生成的。html5

網頁的生成過程,大體能夠分紅五步。java

  1. HTML代碼轉化成DOM
  2. CSS代碼轉化成CSSOM(CSS Object Model)
  3. 結合DOM和CSSOM,生成一棵渲染樹(包含每一個節點的視覺信息)
  4. 生成佈局(layout),即將全部渲染樹的全部節點進行平面合成
  5. 將佈局繪製(paint)在屏幕上

這五步裏面,第一步到第三步都很是快,耗時的是第四步和第五步。git

"生成佈局"(flow)和"繪製"(paint)這兩步,合稱爲"渲染"(render)。github

2、重排和重繪

網頁生成的時候,至少會渲染一次。用戶訪問的過程當中,還會不斷從新渲染。web

如下三種狀況,會致使網頁從新渲染。瀏覽器

  • 修改DOM
  • 修改樣式表
  • 用戶事件(好比鼠標懸停、頁面滾動、輸入框鍵入文字、改變窗口大小等等)

從新渲染,就須要從新生成佈局和從新繪製。前者叫作"重排"(reflow),後者叫作"重繪"(repaint)。

須要注意的是,"重繪"不必定須要"重排",好比改變某個網頁元素的顏色,就只會觸發"重繪",不會觸發"重排",由於佈局沒有改變。可是,"重排"必然致使"重繪",好比改變一個網頁元素的位置,就會同時觸發"重排"和"重繪",由於佈局改變了。

3、對於性能的影響

重排和重繪會不斷觸發,這是不可避免的。可是,它們很是耗費資源,是致使網頁性能低下的根本緣由。

提升網頁性能,就是要下降"重排"和"重繪"的頻率和成本,儘可能少觸發從新渲染。

前面提到,DOM變更和樣式變更,都會觸發從新渲染。可是,瀏覽器已經很智能了,會盡可能把全部的變更集中在一塊兒,排成一個隊列,而後一次性執行,儘可能避免屢次從新渲染。

div.style.color = 'blue'; div.style.marginTop = '30px'; 

上面代碼中,div元素有兩個樣式變更,可是瀏覽器只會觸發一次重排和重繪。

若是寫得很差,就會觸發兩次重排和重繪。

div.style.color = 'blue'; var margin = parseInt(div.style.marginTop); div.style.marginTop = (margin + 10) + 'px'; 

上面代碼對div元素設置背景色之後,第二行要求瀏覽器給出該元素的位置,因此瀏覽器不得不當即重排。

通常來講,樣式的寫操做以後,若是有下面這些屬性的讀操做,都會引起瀏覽器當即從新渲染。

  • offsetTop/offsetLeft/offsetWidth/offsetHeight
  • scrollTop/scrollLeft/scrollWidth/scrollHeight
  • clientTop/clientLeft/clientWidth/clientHeight
  • getComputedStyle()

因此,從性能角度考慮,儘可能不要把讀操做和寫操做,放在一個語句裏面。

 // bad div.style.left = div.offsetLeft + 10 + "px"; div.style.top = div.offsetTop + 10 + "px";  // good var left = div.offsetLeft; var top = div.offsetTop; div.style.left = left + 10 + "px"; div.style.top = top + 10 + "px"; 

通常的規則是:

  • 樣式表越簡單,重排和重繪就越快。
  • 重排和重繪的DOM元素層級越高,成本就越高。
  • table元素的重排和重繪成本,要高於div元素

4、提升性能的九個技巧

有一些技巧,能夠下降瀏覽器從新渲染的頻率和成本。

第一條是上一節說到的,DOM 的多個讀操做(或多個寫操做),應該放在一塊兒。不要兩個讀操做之間,加入一個寫操做。

第二條,若是某個樣式是經過重排獲得的,那麼最好緩存結果。避免下一次用到的時候,瀏覽器又要重排。

第三條,不要一條條地改變樣式,而要經過改變class,或者csstext屬性,一次性地改變樣式。

 // bad var left = 10; var top = 10; el.style.left = left + "px"; el.style.top = top + "px";  // good el.className += " theclassname";  // good el.style.cssText += "; left: " + left + "px; top: " + top + "px;"; 

第四條,儘可能使用離線DOM,而不是真實的網面DOM,來改變元素樣式。好比,操做Document Fragment對象,完成後再把這個對象加入DOM。再好比,使用 cloneNode() 方法,在克隆的節點上進行操做,而後再用克隆的節點替換原始節點。

第五條,先將元素設爲display: none(須要1次重排和重繪),而後對這個節點進行100次操做,最後再恢復顯示(須要1次重排和重繪)。這樣一來,你就用兩次從新渲染,取代了可能高達100次的從新渲染。

第六條,position屬性爲absolutefixed的元素,重排的開銷會比較小,由於不用考慮它對其餘元素的影響。

第七條,只在必要的時候,纔將元素的display屬性爲可見,由於不可見的元素不影響重排和重繪。另外,visibility : hidden的元素只對重繪有影響,不影響重排。

第八條,使用虛擬DOM的腳本庫,好比React等。

第九條,使用 window.requestAnimationFrame()、window.requestIdleCallback() 這兩個方法調節從新渲染(詳見後文)。

5、刷新率

不少時候,密集的從新渲染是沒法避免的,好比scroll事件的回調函數和網頁動畫。

網頁動畫的每一幀(frame)都是一次從新渲染。每秒低於24幀的動畫,人眼就能感覺到停頓。通常的網頁動畫,須要達到每秒30幀到60幀的頻率,才能比較流暢。若是能達到每秒70幀甚至80幀,就會極其流暢。

大多數顯示器的刷新頻率是60Hz,爲了與系統一致,以及節省電力,瀏覽器會自動按照這個頻率,刷新動畫(若是能夠作到的話)。

因此,若是網頁動畫可以作到每秒60幀,就會跟顯示器同步刷新,達到最佳的視覺效果。這意味着,一秒以內進行60次從新渲染,每次從新渲染的時間不能超過16.66毫秒。

一秒之間可以完成多少次從新渲染,這個指標就被稱爲"刷新率",英文爲FPS(frame per second)。60次從新渲染,就是60FPS。

若是想達到60幀的刷新率,就意味着JavaScript線程每一個任務的耗時,必須少於16毫秒。一個解決辦法是使用Web Worker,主線程只用於UI渲染,而後跟UI渲染不相干的任務,都放在Worker線程。

6、開發者工具的Timeline面板

Chrome瀏覽器開發者工具的Timeline面板,是查看"刷新率"的最佳工具。這一節介紹如何使用這個工具。

首先,按下 F12 打開"開發者工具",切換到Timeline面板。

左上角有一個灰色的圓點,這是錄製按鈕,按下它會變成紅色。而後,在網頁上進行一些操做,再按一次按鈕完成錄製。

Timeline面板提供兩種查看方式:橫條的是"事件模式"(Event Mode),顯示從新渲染的各類事件所耗費的時間;豎條的是"幀模式"(Frame Mode),顯示每一幀的時間耗費在哪裏。

先看"事件模式",你能夠從中判斷,性能問題發生在哪一個環節,是JavaScript的執行,仍是渲染?

不一樣的顏色表示不一樣的事件。

  • 藍色:網絡通訊和HTML解析
  • 黃色:JavaScript執行
  • 紫色:樣式計算和佈局,即重排
  • 綠色:重繪

哪一種色塊比較多,就說明性能耗費在那裏。色塊越長,問題越大。

幀模式(Frames mode)用來查看單個幀的耗時狀況。每幀的色柱高度越低越好,表示耗時少。

你能夠看到,幀模式有兩條水平的參考線。

下面的一條是60FPS,低於這條線,能夠達到每秒60幀;上面的一條是30FPS,低於這條線,能夠達到每秒30次渲染。若是色柱都超過30FPS,這個網頁就有性能問題了。

此外,還能夠查看某個區間的耗時狀況。

或者點擊每一幀,查看該幀的時間構成。

7、window.requestAnimationFrame()

有一些JavaScript方法能夠調節從新渲染,大幅提升網頁性能。

其中最重要的,就是 window.requestAnimationFrame() 方法。它能夠將某些代碼放到下一次從新渲染時執行。

function doubleHeight(element) { var currentHeight = element.clientHeight; element.style.height = (currentHeight * 2) + 'px'; } elements.forEach(doubleHeight); 

上面的代碼使用循環操做,將每一個元素的高度都增長一倍。但是,每次循環都是,讀操做後面跟着一個寫操做。這會在短期內觸發大量的從新渲染,顯然對於網頁性能很不利。

咱們可使用window.requestAnimationFrame(),讓讀操做和寫操做分離,把全部的寫操做放到下一次從新渲染。

function doubleHeight(element) { var currentHeight = element.clientHeight; window.requestAnimationFrame(function () { element.style.height = (currentHeight * 2) + 'px'; }); } elements.forEach(doubleHeight); 

頁面滾動事件(scroll)的監聽函數,就很適合用 window.requestAnimationFrame() ,推遲到下一次從新渲染。

$(window).on('scroll', function() { window.requestAnimationFrame(scrollHandler); }); 

固然,最適用的場合仍是網頁動畫。下面是一個旋轉動畫的例子,元素每一幀旋轉1度。

var rAF = window.requestAnimationFrame; var degrees = 0; function update() { div.style.transform = "rotate(" + degrees + "deg)"; console.log('updated to degrees ' + degrees); degrees = degrees + 1; rAF(update); } rAF(update); 

8、window.requestIdleCallback()

還有一個函數window.requestIdleCallback(),也能夠用來調節從新渲染。

它指定只有當一幀的末尾有空閒時間,纔會執行回調函數。

requestIdleCallback(fn); 

上面代碼中,只有當前幀的運行時間小於16.66ms時,函數fn纔會執行。不然,就推遲到下一幀,若是下一幀也沒有空閒時間,就推遲到下下一幀,以此類推。

它還能夠接受第二個參數,表示指定的毫秒數。若是在指定 的這段時間以內,每一幀都沒有空閒時間,那麼函數fn將會強制執行。

requestIdleCallback(fn, 5000); 

上面的代碼表示,函數fn最遲會在5000毫秒以後執行。

函數 fn 能夠接受一個 deadline 對象做爲參數。

requestIdleCallback(function someHeavyComputation(deadline) { while(deadline.timeRemaining() > 0) { doWorkIfNeeded(); } if(thereIsMoreWorkToDo) { requestIdleCallback(someHeavyComputation); } }); 

上面代碼中,回調函數 someHeavyComputation 的參數是一個 deadline 對象。

deadline對象有一個方法和一個屬性:timeRemaining() 和 didTimeout。

(1)timeRemaining() 方法

timeRemaining() 方法返回當前幀還剩餘的毫秒。這個方法只能讀,不能寫,並且會動態更新。所以能夠不斷檢查這個屬性,若是還有剩餘時間的話,就不斷執行某些任務。一旦這個屬性等於0,就把任務分配到下一輪requestIdleCallback

前面的示例代碼之中,只要當前幀還有空閒時間,就不斷調用doWorkIfNeeded方法。一旦沒有空閒時間,可是任務尚未全執行,就分配到下一輪requestIdleCallback

(2)didTimeout屬性

deadline對象的 didTimeout 屬性會返回一個布爾值,表示指定的時間是否過時。這意味着,若是回調函數因爲指定時間過時而觸發,那麼你會獲得兩個結果。

  • timeRemaining方法返回0
  • didTimeout 屬性等於 true

所以,若是回調函數執行了,無非是兩種緣由:當前幀有空閒時間,或者指定時間到了。

function myNonEssentialWork (deadline) { while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) doWorkIfNeeded(); if (tasks.length > 0) requestIdleCallback(myNonEssentialWork); } requestIdleCallback(myNonEssentialWork, 5000); 

上面代碼確保了,doWorkIfNeeded 函數必定會在未來某個比較空閒的時間(或者在指定時間過時後)獲得反覆執行。

requestIdleCallback 是一個很新的函數,剛剛引入標準,目前只有Chrome支持,不過其餘瀏覽器能夠用墊片庫

9、參考連接

相關文章
相關標籤/搜索