雖然咱們能夠經過開發者工具以及lighthouse等工具來查看網站的加載狀況,並按以前咱們說的那些方案作好了優化,但真正用戶打開是否真的如預期通常快,咱們不得而知。一直以來咱們都以實驗室數據爲測試的依據,這些不能表明現場數據,即真實用戶的體驗。css
RUM(Real User Monitoring)所以而誕生。RUM依賴於瀏覽器提供的API來蒐集真實用戶的性能數據,主要包含2個標準文檔,Navigation Timing API 和 Resource Timing API,這兩個API都是基於 High Resolution Time 的規範定製的。git
本文檔將引導你去認識這些API提供的數據,更好的掌握RUM。github
Navigation和Resource Timing之間有部分交集,但二者收集的數據指標仍是不同的。web
先在控制檯嘗試執行一下如下代碼:數組
// 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全稱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協議,則 secureConnectionStart
是 0
,因此咱們能夠作一些兼容性的處理,以下代碼:
// 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
事件,並在事件回調中執行了一些耗時的代碼,你就須要去關注一下 unloadEventStart
和 unloadEventEnd
這兩個指標了。
unload
相關的指標只屬於 Navigation Timing
通常狀況下,跳轉不是什麼大問題,但若是頻繁跳轉,也會或多或少的影響頁面的加載速度,看自身狀況決定是否須要關注着幾個指標 redirectStart
和 redirectEnd
。
文檔加載以後,瀏覽器會解析文檔。通常除非咱們的文檔特別大,解析的耗時纔會影響頁面加載。Navigation Timing提供了相關指標 domInteractive
、domContentLoadedEventStart
、domContentLoadedEventEnd
、domComplete
。
文檔解析相關的指標也只屬於 Navigation Timing。
當文檔和資源都加載完了以後,瀏覽器會觸發一個 load
事件,這時相關的回調函數會依次執行,咱們也能夠去拿到加載時間的指標 loadEventStart
和 loadEventEnd
。
以上兩個指標也只屬於 Navigation Timing
文檔和資源的大小毫無疑問是影響頁面加載性能的關鍵因素。用API也可以拿到這些指標:
transferSize
表示資源傳輸總大小,包含headerencodedBodySize
表示壓縮以後的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
能夠經過名字來獲取對應的條目。對 Navigation 和 Resource Timing 來講,名字就是文檔或資源的URL地址:
// Get timing data for an important hero image var heroImageTime = performance.getEntriesByName("https://somesite.com/images/hero-image.jpg");
跟 getEntriesByType
和 getEntriesByName
不同,getEntries
獲取了全部的條目。
// Get timing data for all entries in the performance entry buffer var allTheTimings = performance.getEntries();
這裏咱們有一個概念沒提到
initiatorType
,有興趣能夠去
MDN 上查詢相關資料
上面咱們提到的三種函數都是一次性獲取性能條目的,但這些都有如下兩個問題:
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. } }
看上去統計上面這些性能指標都很簡單,但還有一些比較棘手的狀況。
並不是全部的性能指標咱們都能獲取到,若是沒有攜帶一些響應頭,某些指標可能就一直是 0
,想要徹底掌握這部分,須要去標準文檔細讀。
當HTTP/1.1的請求帶了 Connection: Keep-Alive
的響應頭的時候,此鏈接會被複用。或者當咱們用的是HTTP/2的時候,一個鏈接會被全部同源資源複用。這些都會影響時間統計,不過咱們不用太刻意去檢查這些,稍微留個心就行了。
對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
是一種非阻塞的請求方式,不用等待服務器響應,只是單方面的數據發送,是收集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,你就能更加了解真是用戶的使用場景。