頁面加載性能之感知真實世界

雖然咱們能夠經過開發者工具以及lighthouse等工具來查看網站的加載狀況,並按以前咱們說的那些方案作好了優化,但真正用戶打開是否真的如預期通常快,咱們不得而知。一直以來咱們都以實驗室數據爲測試的依據,這些不能表明現場數據,即真實用戶的體驗。css

RUM(Real User Monitoring)所以而誕生。RUM依賴於瀏覽器提供的API來蒐集真實用戶的性能數據,主要包含2個標準文檔,Navigation Timing APIResource Timing API,這兩個API都是基於 High Resolution Time 的規範定製的。git

本文檔將引導你去認識這些API提供的數據,更好的掌握RUM。github

瀏覽器中的網絡請求

Navigation和Resource Timing之間有部分交集,但二者收集的數據指標仍是不同的。web

  • Navigation Timing 收集了HTML文檔的性能指標
  • Resource Timing 收集了文檔依賴的資源的性能指標,如:css,js,圖片等等

先在控制檯嘗試執行一下如下代碼:數組

// Get Navigation Timing entries:
performance.getEntriesByType("navigation");

// Get Resource Timing entries:
performance.getEntriesByType("resource");

getEntriesByType 接收一個字符串參數,表示你要獲取的條目類型。想要獲取Navigation Timing的條目,則傳 navigation,另外一個則是傳 resource。以上代碼執行結果,能夠看到相似下方的對象結構:瀏覽器

{
  "connectEnd": 152.20000001136214,
  "connectStart": 85.00000007916242,
  "decodedBodySize": 1270,
  "domComplete": 377.90000007953495,
  "domContentLoadedEventEnd": 236.4000000525266,
  "domContentLoadedEventStart": 236.4000000525266,
  "domInteractive": 236.2999999895692,
  "domainLookupEnd": 85.00000007916242,
  "domainLookupStart": 64.4000000320375,
  "duration": 377.90000007953495,
  "encodedBodySize": 606,
  "entryType": "navigation",
  "fetchStart": 61.600000015459955,
  "initiatorType": "navigation",
  "loadEventEnd": 377.90000007953495,
  "loadEventStart": 377.90000007953495,
  "name": "https://example.com/",
  "nextHopProtocol": "h2",
  "redirectCount": 0,
  "redirectEnd": 0,
  "redirectStart": 0,
  "requestStart": 152.50000008381903,
  "responseEnd": 197.80000008177012,
  "responseStart": 170.00000004190952,
  "secureConnectionStart": 105.80000001937151,
  "startTime": 0,
  "transferSize": 789,
  "type": "navigate",
  "unloadEventEnd": 0,
  "unloadEventStart": 0,
  "workerStart": 0
}

上面的數據看起來很暈,但只要記住一點:你在開發者工具中 Network 看到的 waterflow ,就是用這些數據畫出來的。你也能夠用這些數據繪製相似的圖,用一些工具就能作到,Waterfall 或者 Performance-Bookmarklet緩存

用這些API能夠分析用戶打開一個網站的每個步驟的耗時,你也能夠在js中上去使用這些API來收集真實用戶的性能數據。服務器

網絡請求的生命週期

在你收集完這些性能數據以後,爲了更形象的去理解他們,你須要瞭解一個請求從發起到結束到底經歷了什麼,開發者工具能夠提供這樣的圖表,以下:網絡

如預期的同樣,能夠看到這些步驟:DNS查詢,創建鏈接,TLS握手等等。接下來咱們會對着這份數據依次去介紹它們。架構

如下純屬主觀見解,想要客觀地去學習,回到上方提供的對應API的標準文檔閱讀

DNS查詢

DNS全稱Domain Name System,簡單理解就是根據域名查詢對應的IP地址。取決於你中間的DNS代理層數,可能會花費一些時間。Navigation和Resource Timing都包含如下2個和DNS查詢相關的屬性:

  • domainLookupStart 表明DNS開始查詢的時間
  • domainLookupEnd 表明DNS查詢結束

很簡單,作個減法,咱們就能拿到DNS查詢的耗時。

// Measuring DNS lookup time
var pageNav = performance.getEntriesByType("navigation")[0];
var dnsTime = pageNav.domainLookupEnd - pageNav.domainLookupStart;
要注意一點,這兩個值可能都是 0,當咱們的資源是非同源的時候,假設多是用了第三方的CDN服務,且沒有攜帶 Timing-Allow-Origin 的響應頭。

創建鏈接

在與服務器創建鏈接以後,相關的資源纔會發送到客戶端。若是這個時候用了HTTPS協議,這個創建鏈接的過程就會多一步TLS握手。與此相關的3個指標以下:

  • connectStart 表示鏈接開始創建
  • secureConnectionStart 表示TLS握手開始
  • connectEnd 表示鏈接創建完成(同時也是TLS握手結束)

至於爲何沒有 secureConnectionEnd 這個屬性,應該是TLS的握手是在創建鏈接的最後一步,與 connectEnd 是一個時間點。

若是用的不是HTTPS協議,則 secureConnectionStart0,因此咱們能夠作一些兼容性的處理,以下代碼:

// Quantifying total connection time
var pageNav = performance.getEntriesByType("navigation")[0];
var connectionTime = pageNav.connectEnd - pageNav.connectStart;
var tlsTime = 0; // <-- Assume 0 by default

// Did any TLS stuff happen?
if (pageNav.secureConnectionStart > 0) {
  // Awesome! Calculate it!
  tlsTime = pageNav.connectEnd - pageNav.secureConnectionStart;
}

在DNS查詢和創建鏈接完成後,真正的請求才開始了。

請求與響應

當咱們去思考究竟是什麼影響了請求速度的時候,通常能夠歸類爲如下兩點:

  • 外在因素: 網絡延遲或者帶寬,這些都是開發者沒法掌控的。
  • 內在因素:服務器和客戶端的架構、資源大小等等。

和這部分相關性能指標是重中之重。Navigation和Resource Timing都有以下相關指標:

  • fetchStart 表示瀏覽器開始獲取資源的時間,並不是是說從服務器獲取,而是從檢查緩存開始。
  • workerStart 表示從 [service worker]() 開始獲取資源的時間,若是沒有安裝service worker,則是 0
  • requestStart 表示瀏覽器開始發起網絡請求的時間
  • responseStart 表示服務器響應的第一個字節到達的時間
  • responseEnd 表示服務器響應的最後一個字節到達的時間,即下載完成

咱們能夠用如下代碼來獲取資源下載的時間,以及緩存讀取的時間

// Cache seek plus response time
var pageNav = performance.getEntriesByType("navigation")[0];
var fetchTime = pageNav.responseEnd - pageNav.fetchStart;

// Service worker time plus response time
var workerTime = 0;

if (pageNav.workerStart > 0) {
  workerTime = pageNav.responseEnd - pageNav.workerStart;
}

也能夠去獲取一些對咱們有幫助的組合時間,代碼以下:

// Request time only (excluding unload, redirects, DNS, and connection time)
var requestTime = pageNav.responseStart - pageNav.requestStart;

// Response time only (download)
var responseTime = pageNav.responseEnd - pageNav.responseStart;

// Request + response time
var requestResponseTime = pageNav.responseEnd - pageNav.requestStart;

其餘

以上,咱們已經獲取了大部分重要的性能指標,但還有一些其餘的指標也能夠簡單瞭解一下。

文檔卸載

文檔卸載發生在瀏覽器即將打開新的文檔以前,通常而言,這不會出現什麼大問題。但若是你綁定了 unload 事件,並在事件回調中執行了一些耗時的代碼,你就須要去關注一下 unloadEventStartunloadEventEnd 這兩個指標了。

unload 相關的指標只屬於 Navigation Timing

跳轉

通常狀況下,跳轉不是什麼大問題,但若是頻繁跳轉,也會或多或少的影響頁面的加載速度,看自身狀況決定是否須要關注着幾個指標 redirectStartredirectEnd

文檔解析

文檔加載以後,瀏覽器會解析文檔。通常除非咱們的文檔特別大,解析的耗時纔會影響頁面加載。Navigation Timing提供了相關指標 domInteractivedomContentLoadedEventStartdomContentLoadedEventEnddomComplete

文檔解析相關的指標也只屬於 Navigation Timing。

加載

當文檔和資源都加載完了以後,瀏覽器會觸發一個 load 事件,這時相關的回調函數會依次執行,咱們也能夠去拿到加載時間的指標 loadEventStartloadEventEnd

以上兩個指標也只屬於 Navigation Timing

文檔和資源的大小

文檔和資源的大小毫無疑問是影響頁面加載性能的關鍵因素。用API也可以拿到這些指標:

  • transferSize 表示資源傳輸總大小,包含header
  • encodedBodySize 表示壓縮以後的body大小
  • decodedBodySize 表示解壓以後的body大小

如下代碼能夠獲取到一些其餘信息:

// HTTP header size
var pageNav = performance.getEntriesByType("navigation")[0];
var headerSize = pageNav.transferSize - pageNav.encodedBodySize;

// Compression ratio
var compressionRatio = pageNav.decodedBodySize / pageNav.encodedBodySize;

其實資源和文檔的大小都是開發者本身知道的,能夠經過開發者工具看到,不必定要用API來獲取這些信息。

在代碼中實際應用

基本上上面對這些API都有了一個大體的瞭解,如今咱們能夠在代碼中去收集這些指標數據了。

其餘獲取性能條目的函數

上面咱們講到一個 getEntriesByType 的函數能夠獲取指定類型的性能條目,還有另外兩種:

getEntriesByName

getEntriesByName 能夠經過名字來獲取對應的條目。對 Navigation 和 Resource Timing 來講,名字就是文檔或資源的URL地址:

// Get timing data for an important hero image
var heroImageTime = performance.getEntriesByName("https://somesite.com/images/hero-image.jpg");

getEntries

getEntriesByTypegetEntriesByName 不同,getEntries 獲取了全部的條目。

// Get timing data for all entries in the performance entry buffer
var allTheTimings = performance.getEntries();
這裏咱們有一個概念沒提到 initiatorType,有興趣能夠去 MDN 上查詢相關資料

用 PerformanceObserver 來監聽性能條目

上面咱們提到的三種函數都是一次性獲取性能條目的,但這些都有如下兩個問題:

  • 循環遍歷性能條目的數組(可能很大),會阻塞主線程
  • 沒法統計到新的請求或者新的指標。若是咱們用定時器來嘗試解決這個問題,代價太大,甚至可能會引起渲染衝突,致使jank

PerformanceObserver 就是爲此而誕生的。如下是相關代碼:

// Instantiate the performance observer
var perfObserver = new PerformanceObserver(function(list, obj) {
  // Get all the resource entries collected so far
  // (You can also use getEntriesByType/getEntriesByName here)
  var entries = list.getEntries();

  // Iterate over entries
  for (var i = 0; i < entries.length; i++) {
    // Do the work!
  }
});

// Run the observer
perfObserver.observe({
  // Polls for Navigation and Resource Timing entries
  entryTypes: ["navigation", "resource"]
});

須要注意的是 PerformanceObserver 目前還沒不適用於全部瀏覽器,須要作一些兼容處理:

// Should we even be doing anything with perf APIs?
if ("performance" in window) {
  // OK, yes. Check PerformanceObserver support
  if ("PerformanceObserver" in window) {
    // Observe ALL the performance entries!
  } else {
    // WOMP WOMP. Find another way. Or not.
  }
}

一些陷阱

看上去統計上面這些性能指標都很簡單,但還有一些比較棘手的狀況。

Cross-origins 和 Timing-Allow-Origin 的響應頭

並不是全部的性能指標咱們都能獲取到,若是沒有攜帶一些響應頭,某些指標可能就一直是 0,想要徹底掌握這部分,須要去標準文檔細讀。

持久鏈接會影響時序

當HTTP/1.1的請求帶了 Connection: Keep-Alive 的響應頭的時候,此鏈接會被複用。或者當咱們用的是HTTP/2的時候,一個鏈接會被全部同源資源複用。這些都會影響時間統計,不過咱們不用太刻意去檢查這些,稍微留個心就行了。

不是全部瀏覽器都支持這些API

對Web開發者而言,瀏覽器兼容性是沒法避免的問題。並且 getEntriesByType 這個API函數,若是獲取一個不支持的類型的性能條目,瀏覽器並不會報錯,而是返回空數組,如如下代碼:

// This returns stuff!
performance.getEntriesByType("resource");

// Not so much. :\
performance.getEntriesByType("navigation");

爲此,咱們能夠稍做兼容:

if (performance.getEntriesByType("navigation").length > 0) {
  // Yay, we have Navigation Timing stuff!
}

並不是全部瀏覽器都支持這些API,用的時候儘可能作一些檢測,避免產生一些錯誤的統計。

收集數據

咱們已經知道了如何使用這些API獲取性能指標,但這些數據咱們應該放在哪裏?

使用navigator.sendBeacon

navigator.sendBeacon 是一種非阻塞的請求方式,不用等待服務器響應,只是單方面的數據發送,是收集RUM數據的一個最佳方案,即便頁面關閉,瀏覽器依然會將這些請求發送完成。

// Caution: If you have a _lot_ of performance entries, don't send _everything_ via getEntries. This is just an example.
let rumData = JSON.stringify(performance.getEntries()));

// Check for sendBeacon support:
if ('sendBeacon' in navigator) {
  // Beacon the requested
  if (navigator.sendBeacon('/analytics', rumData)) {
    // sendBeacon worked! We're good!
  } else {
    // sendBeacon failed! Use XHR or fetch instead
  }
} else {
  // sendBeacon not available! Use XHR or fetch instead
}

服務端要獲取這些數據,能夠從post表單中獲取,或者從get的參數中獲取。

navigator.sendBeacon 調用的時候,只是往隊列裏面插入了一個,等待瀏覽器資源空閒,會將請求發送出去。若是資源過大,瀏覽器也可能會拒絕發送。

總結

若是你對這些還不夠自信,千萬不要直接就應用在項目代碼中,建議詳細閱讀相關標準文檔以後,再嘗試應用在項目中。有了這些性能指標數據,咱們能夠隨時修復一些發現的問題。

另外,你也不用把全部指標都存到服務器,選一些本身以爲有用的就好。

本文檔只是一個引導性質的,並不能徹底表明這些API的全部使用方式,建議仍是閱讀如下相關標準文檔(文中連接)。

有了這些API,你就能更加了解真是用戶的使用場景。

參考

https://developers.google.com...

相關文章
相關標籤/搜索