深入理解前端效能監控

TNFE發表於2019-04-08

在同樣的網路環境下,有兩個同樣能滿足你的需求的網站,一個唰的一下就載入出來了,另一個白屏轉圈轉了半天內容才出來,如果讓你選擇,你會用哪一個?

深入理解前端效能監控

頁面的效能問題是前端開發中一個重要環節,但一直以來我們沒有比較好的手段,來檢測頁面的效能。直到W3C效能小組引入的新的API window.performance,目前IE9以上的瀏覽器都支援。它是一個瀏覽器中用於記錄頁面載入和解析過程中關鍵時間點的物件。放置在global環境下,通過JavaScript可以訪問到它。

使用效能API

你可以通過以下方法來探測和相容performance:

var performance = window.performance || 
    window.msPerformance || 
    window.webkitPerformance;
if (performance) {
    // 你的程式碼
}
複製程式碼

先來了解一下performance的結構

深入理解前端效能監控

performance.memory是顯示此刻記憶體佔用情況,它是一個動態值,其中: usedJSHeapSize表示:JS 物件(包括V8引擎內部物件)佔用的記憶體數 totalJSHeapSize表示:可使用的記憶體 jsHeapSizeLimit表示:記憶體大小限制 通常,usedJSHeapSize不能大於totalJSHeapSize,如果大於,有可能出現了記憶體洩漏。

performance.navigation顯示頁面的來源資訊,其中: redirectCount表示:如果有重定向的話,頁面通過幾次重定向跳轉而來,預設為0 type表示頁面開啟的方式, 0 表示 TYPE_NAVIGATENEXT 正常進入的頁面(非重新整理、非重定向等) 1 表示 TYPE_RELOAD 通過 window.location.reload() 重新整理的頁面 2 表示 TYPE_BACK_FORWARD 通過瀏覽器的前進後退按鈕進入的頁面(歷史記錄) 255 表示 TYPE_UNDEFINED 非以上方式進入的頁面

performance.onresourcetimingbufferfull 屬性是一個在resourcetimingbufferfull事件觸發時會被呼叫的 event handler 。它的值是一個手動設定的回撥函式,這個回撥函式會在瀏覽器的資源時間效能緩衝區滿時執行。

performance.timeOrigin是一系列時間點的基準點,精確到萬分之一毫秒。

performance.timing是一系列關鍵時間點,它包含了網路、解析等一系列的時間資料。

深入理解前端效能監控

下面是對這些時間點進行解釋

timing: {
        // 同一個瀏覽器上一個頁面解除安裝(unload)結束時的時間戳。如果沒有上一個頁面,這個值會和fetchStart相同。
	navigationStart: 1543806782096,

	// 上一個頁面unload事件丟擲時的時間戳。如果沒有上一個頁面,這個值會返回0。
	unloadEventStart: 1543806782523,

	// 和 unloadEventStart 相對應,unload事件處理完成時的時間戳。如果沒有上一個頁面,這個值會返回0。
	unloadEventEnd: 1543806782523,

	// 第一個HTTP重定向開始時的時間戳。如果沒有重定向,或者重定向中的一個不同源,這個值會返回0。
	redirectStart: 0,

	// 最後一個HTTP重定向完成時(也就是說是HTTP響應的最後一個位元直接被收到的時間)的時間戳。
	// 如果沒有重定向,或者重定向中的一個不同源,這個值會返回0. 
	redirectEnd: 0,

	// 瀏覽器準備好使用HTTP請求來獲取(fetch)文件的時間戳。這個時間點會在檢查任何應用快取之前。
	fetchStart: 1543806782096,

	// DNS 域名查詢開始的UNIX時間戳。
        //如果使用了持續連線(persistent connection),或者這個資訊儲存到了快取或者本地資源上,這個值將和fetchStart一致。
	domainLookupStart: 1543806782096,

	// DNS 域名查詢完成的時間.
	//如果使用了本地快取(即無 DNS 查詢)或持久連線,則與 fetchStart 值相等
	domainLookupEnd: 1543806782096,

	// HTTP(TCP) 域名查詢結束的時間戳。
        //如果使用了持續連線(persistent connection),或者這個資訊儲存到了快取或者本地資源上,這個值將和 fetchStart一致。
	connectStart: 1543806782099,

	// HTTP(TCP) 返回瀏覽器與伺服器之間的連線建立時的時間戳。
        // 如果建立的是持久連線,則返回值等同於fetchStart屬性的值。連線建立指的是所有握手和認證過程全部結束。
	connectEnd: 1543806782227,

	// HTTPS 返回瀏覽器與伺服器開始安全連結的握手時的時間戳。如果當前網頁不要求安全連線,則返回0。
	secureConnectionStart: 1543806782162,

	// 返回瀏覽器向伺服器發出HTTP請求時(或開始讀取本地快取時)的時間戳。
	requestStart: 1543806782241,

	// 返回瀏覽器從伺服器收到(或從本地快取讀取)第一個位元組時的時間戳。
        //如果傳輸層在開始請求之後失敗並且連線被重開,該屬性將會被數製成新的請求的相對應的發起時間。
	responseStart: 1543806782516,

	// 返回瀏覽器從伺服器收到(或從本地快取讀取,或從本地資源讀取)最後一個位元組時
        //(如果在此之前HTTP連線已經關閉,則返回關閉時)的時間戳。
	responseEnd: 1543806782537,

	// 當前網頁DOM結構開始解析時(即Document.readyState屬性變為“loading”、相應的 readystatechange事件觸發時)的時間戳。
	domLoading: 1543806782573,

	// 當前網頁DOM結構結束解析、開始載入內嵌資源時(即Document.readyState屬性變為“interactive”、相應的readystatechange事件觸發時)的時間戳。
	domInteractive: 1543806783203,

	// 當解析器傳送DOMContentLoaded 事件,即所有需要被執行的指令碼已經被解析時的時間戳。
	domContentLoadedEventStart: 1543806783203,

	// 當所有需要立即執行的指令碼已經被執行(不論執行順序)時的時間戳。
	domContentLoadedEventEnd: 1543806783216,

	// 當前文件解析完成,即Document.readyState 變為 'complete'且相對應的readystatechange 被觸發時的時間戳
	domComplete: 1543806783796,

	// load事件被髮送時的時間戳。如果這個事件還未被髮送,它的值將會是0。
	loadEventStart: 1543806783796,

	// 當load事件結束,即載入事件完成時的時間戳。如果這個事件還未被髮送,或者尚未完成,它的值將會是0.
	loadEventEnd: 1543806783802
}
複製程式碼

這些引數非常有用,可以幫助我們獲取頁面的Domready時間、onload時間、白屏時間等,以及單個頁面資源在從傳送請求到獲取到rsponse各階段的效能引數。

對我們比較有用的頁面效能資料大概包括如下幾個,這些引數是通過上面的performance.timing各個屬性的差值組成的,它是精確到毫秒的一個值,計算方法如下:

  • 重定向耗時:redirectEnd - redirectStart
  • DNS查詢耗時 :domainLookupEnd - domainLookupStart
  • TCP連結耗時 :connectEnd - connectStart
  • HTTP請求耗時 :responseEnd - responseStart
  • 解析dom樹耗時 : domComplete - domInteractive
  • 白屏時間 :responseStart - navigationStart
  • DOMready時間 :domContentLoadedEventEnd - navigationStart
  • onload時間:loadEventEnd - navigationStart,也即是onload回撥函式執行的時間。

如何優化?

**重定向優化:**重定向的型別分三種,301(永久重定向),302(臨時重定向),304(Not Modified)。304是用來優化快取,非常有用,而前兩種應該儘可能的避免,凡是遇到需要重定向跳轉程式碼的程式碼,可以把重定向之後的地址直接寫到前端的html或JS中,可以減少客戶端與服務端的通訊過程,節省重定向耗時。

**DNS優化:**一般來說,在前端優化中與 DNS 有關的有兩點: 一個是減少DNS的請求次數,另一個就是進行DNS預獲取(Prefetching ) 。典型的一次DNS解析需要耗費 20-120 毫秒(移動端會更慢),減少DNS解析的次數是個很好的優化方式,儘量把各種資源放在一個cdn域名上。DNS Prefetching 是讓具有此屬性的域名不需要使用者點選連結就在後臺解析,而域名解析和內容載入是序列的網路操作,所以這個方式能減少使用者的等待時間,提升使用者體驗 。新版的瀏覽器會對頁面中和當前域名(正在瀏覽網頁的域名)不在同一個域的域名進行預獲取,並且快取結果,這就是隱式的 DNS Prefetch。如果想對頁面中沒有出現的域進行預獲取,那麼就要使用顯示的 DNS Prefetch 了。下圖是DNS Prefetch的方法:

<html>
<head>
  <title>騰訊網</title>
  <link rel="dns-prefetch" href="//mat1.gtimg.com"  />
  <link rel="dns-prefetch" href="//inews.gtimg.com"  />
  <link rel="dns-prefetch" href="//wx.qlogo.cn"  />
  <link rel="dns-prefetch" href="//coral.qq.com" />
  <link rel="dns-prefetch" href="//pingjs.qq.com"  />
複製程式碼

**TCP請求優化:**TCP的優化大都在伺服器端,前端能做的就是儘量減少TCP的請求數,也就是減少HTTP的請求數量。http 1.0 預設使用短連線,也是TCP的短連線,也就是客戶端和服務端每進行一次http操作,就建立一次連線,任務結束就中斷連線。這個過程中有3次TCP請求握手和4次TCP請求釋放。減少TCP請求的方式有兩種,一種是資源合併,對於頁面內的圖片、css和js進行合併,減少請求量。另一種使用長連結,使用http1.1,在HTTP的響應頭會加上 Connection:keep-alive,當一個網頁開啟完成之後,連線不會馬上關閉,再次訪問這個服務時,會繼續使用這個長連線。這樣就大大減少了TCP的握手次數和釋放次數。或者使用Websocket進行通訊,全程只需要建立一次TCP連結。

**HTTP請求優化:**使用內容分發網路(CDN)和減少請求。使用CDN可以減少網路的請求時延,CDN的域名不要和主站的域名一樣,這樣會防止訪問CDN時還攜帶主站cookie的問題,對於網路請求,可以使用fetch傳送無cookie的請求,減少http包的大小。也可以使用本地快取策略,儘量減少對伺服器資料的重複獲取。

**渲染優化:**在瀏覽器端的渲染過程,如大型框架,vue和react,它的模板其實都是在瀏覽器端進行渲染的,不是直出的html,而是要走框架中相關的框架程式碼才能去渲染出頁面,這個渲染過程對於首屏就有較大的損耗,白屏的時間會有所增加。在必要的情況下可以在服務端進行整個html的渲染,從而將整個html直出到我們的瀏覽器端,而非在瀏覽器端進行渲染。

深入理解前端效能監控

還有一個問題就是,在預設情況下,JavaScript 執行會“阻止解析器”,當瀏覽器遇到一個 script 外鏈標記時,DOM 構建將暫停,會將控制權移交給 JavaScript 執行時,等指令碼下載執行完畢,然後再繼續構建 DOM。而且內聯指令碼始終會阻止解析器,除非編寫額外程式碼來推遲它們的執行。我們可以把 script 外鏈加入到頁面底部,也可以使用 defer 或 async 延遲執行。defer 和 async 的區別就是 defer 是有序的,程式碼的執行按在html中的先後順序,而 async 是無序的,只要下載完畢就會立即執行。或者使用非同步的程式設計方法,比如settimeout,也可以使用多線webworker,它們不會阻礙 DOM 的渲染。

<script async type="text/javascript" src="app1.js"></script>
<script defer type="text/javascript" src="app2.js"></script>
``` 

## 資源效能API
performance.timing記錄的是用於分析頁面整體效能指標。如果要獲取個別資源(例如JS、圖片)的效能指標,就需要使用Resource Timing API。
**performance.getEntries()**方法,包含了所有靜態資源的陣列列表;每一項是一個請求的相關引數有name,type,時間等等。下圖是chrome顯示騰訊網的相關資源列表。


![](https://user-gold-cdn.xitu.io/2019/4/8/169fab40a349cf36?w=784&h=582&f=png&s=29220)

可以看到,與 performance.timing 對比: 沒有與 DOM 相關的屬性,新增了`name`、`entryType`、`initiatorType`和`duration`四個屬性。它們是
* name表示:資源名稱,也是資源的絕對路徑,可以通過performance.getEntriesByName(name屬性的值),來獲取這個資源載入的具體屬性。
* entryType表示:資源型別 "resource",還有“navigation”, “mark”, 和 “measure”另外3種。

    
![](https://user-gold-cdn.xitu.io/2019/4/8/169fab43a85a2e4d?w=642&h=215&f=png&s=6807)

* initiatorType表示:請求來源 "link",即表示<link> 標籤,還有“script”即 <script>,“img”即<img>標籤,“css”比如background的url方式載入資源以及“redirect”即重定向 等。

   
![](https://user-gold-cdn.xitu.io/2019/4/8/169fab468959d830?w=715&h=327&f=png&s=10856)

* duration表示:載入時間,是一個毫秒數字。
受同源策略影響,跨域資源獲取到的時間點,通常為0,如果需要更詳細準確的時間點,可以單獨請求資源通過`performance.timing`獲得。或者資源伺服器開啟響應頭Timing-Allow-Origin,新增指定來源站點,如下所示:
複製程式碼

Timing-Allow-Origin: qq.com

## 方法集合
除了`performance.getEntries`之外,`performance`還包含一系列有用的方法。如下圖


![](https://user-gold-cdn.xitu.io/2019/4/8/169fab4b218c3601?w=453&h=247&f=png&s=8422)

**performance.now()**
`performance.now()` 返回一個當前頁面執行的時間的時間戳,用來精確計算程式執行時間。與 `Date.now()` 不同的是,它使用了一個浮點數,返回了以毫秒為單位,小數點精確到微秒級別的時間,更加精準。並且不會受系統程式執行阻塞的影響,`performance.now()` 的時間是以恆定速率遞增的,不受系統時間的影響(系統時間可被人為或軟體調整)。`performance.timing.navigationStart + performance.now()` 約等於 `Date.now()`。
複製程式碼

let t0 = window.performance.now(); doSomething(); let t1 = window.performance.now(); console.log("doSomething函式執行了" + (t1 - t0) + "毫秒.")

 通過這個方法,我們可以用來測試某一段程式碼執行了多少時間。

**performance.mark()**
mark方法用來自定義新增標記時間。使用方法如下:
複製程式碼
var nameStart = 'markStart';
var nameEnd   = 'markEnd';
// 函式執行前做個標記
window.performance.mark(nameStart);
for (var i = 0; i < n; i++) {
    doSomething
}
// 函式執行後再做個標記
window.performance.mark(nameEnd);
// 然後測量這個兩個標記間的時間距離,並儲存起來
var name = 'myMeasure';
window.performance.measure(name, nameStart, nameEnd);
複製程式碼
 儲存後的值可以通過 **performance.getEntriesByname( 'myMeasure' )**或者 **performance.getEntriesByType**('measure')查詢。

**Performance.clearMeasures()**
從瀏覽器的效能輸入緩衝區中移除自定義新增的 measure

**Performance.getEntriesByName()**
返回一個 PerformanceEntry 物件的列表,基於給定的 name 和 entry type

**Performance.getEntriesByType()**
返回一個 PerformanceEntry 物件的列表,基於給定的 entry type

**Performance.measure()**
在瀏覽器的指定 start mark 和 end mark 間的效能輸入緩衝區中建立一個指定名稱的時間戳,見上例

**Performance.toJSON()** 
是一個 JSON 格式轉化器,返回 Performance 物件的 JSON 物件

## 資源緩衝區監控
**Performance.setResourceTimingBufferSize()**
設定當前頁面可快取的最大資源資料個數,entryType為resource的資源資料個數。超出時,會清空所有entryType為resource的資源資料。引數為整數(maxSize)。配合performance.onresourcetimingbufferfull事件可以有效監控資源緩衝區。當entryType為resource的資源數量超出設定值的時候會觸發該事件。
**Performance.clearResourceTimings()**
從瀏覽器的效能資料緩衝區中移除所有的 entryType 是 "resource" 的 performance entries
下面是mdn上關於這個屬性的一個demo。這個demo的主要內容是當緩衝區內容滿時,呼叫buffer_full函式。
複製程式碼

function buffer_full(event) { console.log("WARNING: Resource Timing Buffer is FULL!"); performance.setResourceTimingBufferSize(200); } function init() { // Set a callback if the resource buffer becomes filled performance.onresourcetimingbufferfull = buffer_full; }

``` 使用performance的這些屬性和方法,能夠準確的記錄下我們想要的時間,再加上日誌採集等功能的輔助,我們就能很容易的掌握自己網站的各項效能指標了。

相容性

目前主流瀏覽器雖然都已支援performance物件,但是並不能支援它上面的全部屬性和方法,有些細微的差別。本文主要依據chrome和qq瀏覽器測試了相關屬性和方法,均可使用。

我們做了什麼?(劃重點)

現在的很多效能監控分析工具都是通過資料上報來實現的,不能及時有效的反饋頁面的效能問題,只能在使用者使用之後上報(問題出現之後)才能知道。所以基於新聞前端團隊基於performance API做了一款實時檢視效能的的工具,它並能給出詳細的報表,在開發階段把效能問題給解決掉。

superProfiler**【外部開源流程中】**

深入理解前端效能監控

它是一款JavaScript效能監控工具庫,通過指令碼引用,載入展示在頁面右側,無須依賴任何庫和指令碼,可以實時檢視當前頁面的FPS、程式碼執行耗時、記憶體佔用以及當前頁面的網路效能,資源佔用。

深入理解前端效能監控

還能檢視最近的(10次)頁面效能的平均數。點選“生成報表”按鈕會生成更詳細的資料包表概覽。

深入理解前端效能監控

小結

Performance API 用來做前端效能監控非常有用,它提供了很多方便測試我們程式效能的介面。比如mark和measure。很多優秀的框架也用到了這個API進行測試。它裡面就頻繁用到了mark和measure來測試程式效能。所以想要開發高效能的web程式,瞭解Performace API還是非常重要的。最後通過superProfiler工具可以更快更便捷的查詢出效能問題,針對性的擊破問題,提高開發效率,提升使用者體驗。當然這只是前端效能優化的第一步,道阻且長。希望大家提出問題和指出疑問,一起進步。

作者:TNFE 大鵬哥

團隊推廣

最後,騰訊新聞TNFE前端團隊為前端開發人員整理出了小程式以及web前端技術領域的最新優質內容,每週更新✨,歡迎star,github地址:github.com/Tnfe/TNFE-W…

相關文章