本教程解釋瞭如何使用Performance API來記錄真實用户訪問你的應用程序的統計數據。
使用瀏覽器的DevTools來評估web應用性能是很有用的,但要復現現實世界的使用情況並不容易。因為人們在不同地點使用不同的設備、瀏覽器和網絡,都會有不同的體驗。
Performance API介紹
Performance API使用一個緩衝區,在你的網頁生命週期的確定節點上,在對象屬性中記錄類似DevTool的指標。這些節點包括:
- 頁面導航:記錄頁面加載重定向、連接、握手、DOM事件等等。
- 資源加載:記錄資源加載,比如圖像、CSS、腳本以及Ajax調用。
- 繪製指標:記錄瀏覽器渲染信息。
- 自定義:記錄任意的應用處理時間,來找到運行慢的函數。
所有的API都可以在客户端的JavaScript中使用,包括Web Workers。你可以用以下方法檢測API支持情況:
if ('performance' in window) {
// call Performance APIs
}
注意:儘管Safari實現了大部分的API,但Safari並不支持所有的方法。
自定義performance API也被複制到了:
- Node.js 內置
performance_hook模塊,以及 - Deno performance API,(使用它的腳本必須以
--allow-hrtime權限運行)。
Date()不夠好嗎
你可能已經看到過使用Date()函數來記錄經過時間的例子。比如:
const start = new Date();
// ... run code ...
const elapsed = new Date() - start;
然而,Date()的計算被限制在最接近的毫秒數,並且是基於系統時間。而系統時間可以在任何時候被操作系統更新。
Performance API使用獨立的、高精度的定時器,其可以在幾毫秒的時間內記錄。它還提供其他方式無法記錄的指標,如重定向和DNS查詢時間。
記錄性能指標
如果你可以在某處記錄的話,在客户端代碼中記錄性能指標是非常有用的。你可以使用Fetch/XMLHttpRequest請求,或者使用Beacon API,來發送統計數據到服務端進行分析。
另外,大多數分析系統提供類似的事件API來記錄時間。比如説,Google分析的User Timings API可以通過傳遞類別'pageload'、變量名'DOMready'和一個值,來記錄DOMContentLoaded的時間:
const pageload = performance.getEntriesByType( 'navigation' )[0];
ga('send', 'timing', 'pageload', 'DOMready', pageload.domContentLoadedEventStart);
這個例子使用了Page Navigation Timing API,那麼就從這開始吧。
頁面導航時間
在快速連接上測試你的網站,並不能代表用户體驗。瀏覽器DevTools的NetWork標籤允許你限制速度,但它不能模擬糟糕的或間歇性的信號。
Navigation Timing API將單獨的PerformanceNavigationTiming對象放入到性能緩衝區中。它包含有關重定向、加載時間、文件大小、DOM事件等的信息。
通過運行以下代碼來訪問該對象:
const pagePerf = performance.getEntriesByType('navigation');
或者傳遞頁面URL(window.location)到 getEntriesByName() 方法中,來訪問該對象:
const pagePerf = performance.getEntriesByName(window.location);
兩者都返回一個數組,該數組擁有一個具有隻讀屬性的對象的單一元素。比如説:
[
{
name: "<https://site.com/>",
initiatorType: "navigation",
entryType: "navigation",
initiatorType: "navigation",
type: "navigate",
nextHopProtocol: "h2",
startTime: 0
...
}
]
該對象包含資源識別屬性:
| 屬性 | 描述 |
|---|---|
| name | 資源URL |
| entryType | 性能類型 — "navigation"代表一個頁面,"resource"代表一個資源 |
| initiatorType | 啓動下載的資源 — "navigation"代表一個頁面 |
| nextHopProtocol | 網絡協議 |
| serverTiming | PerformanceServerTiming對象數組 |
注意:performanceServerTiming的name、description和duration等指標由服務器響應寫入HTTPServer-Timing頭部。
該對象包括相對於頁面加載開始的以毫秒為單位的資源時間屬性。通常情況下,時間會按照這個順序來展示:
| 屬性 | 描述 |
|---|---|
| startTime | 頁面開始獲取時的時間戳,從0開始 |
| workerStart | 啓動Service Worker之前的時間戳 |
| redirectStart | 首次重定向的時間戳 |
| redirectEnd | 收到最後重定向最後一個字節後的時間戳 |
| fetchStart | 資源開始獲取前的時間戳 |
| domainLookupStart | DNS查詢前的時間戳 |
| domainLookupEnd | DNS查詢後的時間戳 |
| connectStart | 建立服務器連接前的時間戳 |
| connectEnd | 建立服務器連接後的時間戳 |
| secureConnectionStart | SSL握手前的時間戳 |
| requestStart | 瀏覽器請求前的時間戳 |
| responseStart | 瀏覽器收到第一個字節數據的時間戳 |
| responseEnd | 收到最後一個字節數據後的時間戳 |
| duration | 從startTime到responseEnd所經過的時間 |
該對象包括以字節為單位的下載大小屬性:
| 屬性 | 描述 |
|---|---|
| transferSize | 資源大小,包括頭部和主體 |
| encodedBodySize | 解壓前的資源主體大小 |
| decodedBodySize | 解壓後的資源主體大小 |
最後,該對象包括進一步的導航和DOM事件屬性(在Safari中不可用):
| 屬性 | 描述 |
|---|---|
| type | "navigate"、"reload"、"back_forward" |
| 或者 "prerender" | |
| redirectCount | 重定向的次數 |
| unloadEventStart | 前一個文檔的unload事件之前的時間戳 |
| unloadEventEnd | 前一個文檔的unload事件之後的時間戳 |
| domInteractive | HTML解析和DOM構建完成時的時間戳 |
| domContentLoadedEventStart | 運行DOMContentLoaded事件處理器前的時間戳 |
| domContentLoadedEventEnd | 運行DOMContentLoaded事件處理器後的時間戳 |
| domComplete | DOM構建和DOMContentLoaded事件完成後的時間戳 |
| loadEventStart | 頁面load事件發生前的時間戳 |
| loadEventEnd | 頁面load事件發生後的時間戳,所有資源已經被下載 |
在頁面完全加載後記錄頁面加載指標的例子如下:
'performance' in window && window.addEventListener('load', () => {
const
pagePerf = performance.getEntriesByName(window.location)[0],
pageDownload = pagePerf.duration,
pageDomComplete = pagePerf.domComplete;
});
頁面資源時間
每當頁面加載圖片、字體、CSS文件、JavaScript文件等資產時,Resource Timing API將PerformanceResourceTiming對象放入性能緩衝區中,可以這麼運行:
const resPerf = performance.getEntriesByType('resource');
這樣會返回資源時間的對象數組。這些屬性與上面顯示的頁面時間相同,但沒有導航和DOM事件信息。
下面是返回結果的示例:
[
{
name: "<https://site.com/style.css>",
entryType: "resource",
initiatorType: "link",
fetchStart: 150,
duration: 300
...
},
{
name: "<https://site.com/script.js>",
entryType: "resource",
initiatorType: "script",
fetchStart: 302,
duration: 112
...
},
...
]
單一資源可以傳遞資源URL到.getEntriesByName()方法進行測試:
const resourceTime = performance.getEntriesByName('<https://site.com/style.css>');
這會返回單一元素的數組:
[
{
name: "<https://site.com/style.css>",
entryType: "resource",
initiatorType: "link",
fetchStart: 150,
duration: 300
...
}
]
可以使用API來報告加載時間以及每個CSS文件解壓後的大小:
// array of CSS files, load times, and file sizes
const css = performance.getEntriesByType('resource')
.filter(r => r.initiatorType === 'link' && r.name.includes('.css'))
.map(r => ({
name: r.name,
load: r.duration + 'ms',
size: r.decodedBodySize + ' bytes'
}));
CSS數組現在為每個CSS文件包含一個對象。比如:
[
{
name: "<https://site.com/main.css>",
load: "155ms",
size: "14304 bytes"
},
{
name: "<https://site.com/grid.css>",
load: "203ms",
size: "5696 bytes"
}
]
注意:load的大小為0表示該資源已經被緩存了。
至少有150個資源指標對象將被記錄到性能緩衝區。你可以用.setResourceTimingBufferSize(N)方法定義一個指定數字。比如:
// record 500 resources
performance.setResourceTimingBufferSize(500);
現有的指標可以用.clearResourceTimings()方法清除。
瀏覽器繪製時間
First Contentful Paint (FCP)測量用户導航到你的頁面後渲染內容所需的時間。Chrome的DevTool的Lighthouse標籤展示了該指標。谷歌認為FCP時間少於兩秒是好的,你的頁面將比75%的頁面展現的更快。
當發生以下兩種情況時,Paint Timing API將兩個記錄也就是兩個PerformancePaintTiming對象推入性能緩衝區:
first-paint發生:瀏覽器繪製首個像素,以及first-contentful-paint發生:瀏覽器繪製首個DOM元素
當運行下面代碼時,兩個對象以數組形式返回:
const paintPerf = performance.getEntriesByType('paint');
返回結果示例:
[
{
"name": "first-paint",
"entryType": "paint",
"startTime": 125
},
{
"name": "first-contentful-paint",
"entryType": "paint",
"startTime": 127
}
]
startTime是相對於初始化頁面加載的時間。
用户時間
Performance API可以用來為你自己的應用功能計時。所有的用户時間方法都可以在客户端的JavaScript、Web Workers、Deno和Node.js中使用。
注意,Node.js腳本必須加載Performance hooks(perf_hooks)模塊。**
CommonJSrequire語法:
const { performance } = require('perf_hooks');
或者ESMimport語法:
import { performance } from 'perf_hooks';
最簡單的選擇是[performance.now()](<https://developer.mozilla.org/docs/Web/API/Performance/now>),其會從程序的生命週期開始,返回一個高精度時間戳。
你可以使用performance.now()作為簡單的計時器。比如説:
const start = performance.now();
// ... run code ...
const elapsed = performance.now() - start;
注意,不標準的timeOrigin屬性返回一個時間戳。可以用於Node.js和瀏覽器JavaScript,但不能用於IE和Safari。
當管理多個定時器時,performance.now()很快就變得不切實際。.mark()方法添加一個名為PerformanceMark object對象到性能緩衝區。比如説:
performance.mark('script:start');
performance.mark('p1:start');
// ... run process 1 ...
performance.mark('p1:end');
performance.mark('p2:start');
// ... run process 2 ...
performance.mark('p2:end');
performance.mark('script:end');
下列代碼返回mark對象數組:
const marks = performance.getEntriesByType('mark');
數組裏的對象擁有entryType、name和startTime屬性:
[
{
entryType: "mark",
name: "script:start",
startTime: 100
},
{
entryType: "mark",
name: "p1:start",
startTime: 200
},
{
entryType: "mark",
name: "p1:end",
startTime: 300
},
...
]
兩個標記之間的時間可以用.measure()方法來計算。它傳遞一個測量名稱,開始標記名稱(或者null),以及結束標記名稱(或者null):
performance.measure('p1', 'p1:start', 'p1:end');
performance.measure('script', null, 'script:end');
每次調用都會向性能緩衝區推送一個帶有計算持續時間的PerformanceMeasure對象。測量數組可以通過運行以下代碼進行訪問:
const measures = performance.getEntriesByType('measure');
返回示例:
[
{
entryType: "measure",
name: "p1",
startTime: 200,
duration: 100
},
{
entryType: "measure",
name: "script",
startTime: 0,
duration: 500
}
]
標記或測量對象可以使用.getEntriesByName()方法按名稱檢索:
performance.getEntriesByName('p1');
其他方法:
- .getEntries():返回所有性能條目的數組。
- [.clearMarks([name])](https://developer.mozilla.org...):清除指定名稱的標記(不指定名稱則清除所有標記)。
- [.clearMeasures([name])](https://developer.mozilla.org...):清除指定名稱的測量(不指定名稱則清除所有測量)。
PerformanceObserver可以監聽緩衝區的更改,當指定對象出現時執行函數。觀察者函數使用兩個參數定義:
list:觀察者條目observer(可選):觀察者對象
function performanceHandler(list, observer) {
list.getEntries().forEach(entry => {
console.log(`name : ${ entry.name }`);
console.log(`type : ${ entry.type }`);
console.log(`duration: ${ entry.duration }`);
// other code, e.g.
// send data via an Ajax request
});
}
該函數傳遞一個新的PerformanceObserver對象。.observe()方法設置可觀察的entryTypes(一般來説是mark,measure或者resource):
let observer = new PerformanceObserver(performanceHandler);
observer.observe({entryTypes: ['mark', 'measure']});
每當有新的標記或測量對象被推送到性能緩衝區,performanceHandler()函數就會運行。
總結
Performance API提供了一種方法來測量網站和應用程序的速度,這些設備是由不同地點的人在一系列連接上使用的實際設備。它使每個人都能輕鬆地整理出類似DevTool的指標,並識別潛在的瓶頸。
以上就是本文的全部內容,如果對你有所幫助,歡迎點贊、收藏、轉發~