Performance API簡介

Performance API簡介

Performance API測量實時Web應用程式在真實使用者裝置和網路連線上的響應能力。它可以通過以下方式幫助識別客戶端和伺服器端程式碼中的瓶頸:

  • 使用者計時:客戶端JavaScript函式效能的自定義測量
  • 繪製時間:瀏覽器渲染指標
  • 資源計時:資產和Ajax呼叫的載入效能
  • 導航時間:頁面載入指標,包括重定向、DNS查詢、DOM準備情況等

API解決了與典型效能評估相關的幾個問題:

  1. 開發人員經常在連線到快速網路的高階PC上測試應用程式。DevTools可以模擬速度較慢的裝置,但當大多數客戶執行連線到機場WiFi的兩年前的移動裝置時,它並不總是突出現實世界的問題。
  2. 谷歌分析等第三方選項經常被阻止,導致結果和假設出現偏差。在某些國家/地區,您可能還會遇到隱私問題。
  3. Performance API可以比Date()等方法更準確地衡量各種指標。

以下部分描述了您可以使用Performance API的方式。建議您瞭解一些JavaScript頁面載入指標的知識。

  1. Performance API可用性
  2. 自定義效能測量
  3. 資源效能測量
  4. 導航效能測量
  5. 效能記錄與分析

Performance API可用性

大多數現代瀏覽器都支援Performance API——包括IE10和IE11(甚至IE9也有有限的支援)。您可以使用以下方法檢測API的存在:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if ('performance' in window) {
// use Performance API
}
if ('performance' in window) { // use Performance API }
if ('performance' in window) {
// use Performance API
}

百分比的Polyfill API是不可能的,所以要小心缺少瀏覽器。如果您的90%的使用者都樂於使用Internet Explorer 8進行瀏覽,那麼您只能衡量10%的客戶端具有更強大的應用程式。

該API可以在Web Workers中使用,它提供了一種在後臺執行緒中執行復雜計算而無需停止瀏覽器操作的方法。

大多數API方法都可以通過標準perf_hooks模組在伺服器端Node.js中使用:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Node.js performance
import { performance } from 'node:perf_hooks';
// or in Common JS: const { performance } = require('node:perf_hooks');
console.log( performance.now() );
// Node.js performance import { performance } from 'node:perf_hooks'; // or in Common JS: const { performance } = require('node:perf_hooks'); console.log( performance.now() );
// Node.js performance
import { performance } from 'node:perf_hooks';
// or in Common JS: const { performance } = require('node:perf_hooks');
console.log( performance.now() );

Deno提供了標準的Performance API

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Deno performance
console.log( performance.now() );
// Deno performance console.log( performance.now() );
// Deno performance
console.log( performance.now() );

您需要使用--allow-hrtime許可權執行指令碼以啟用高解析度時間測量:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
deno run --allow-hrtime index.js
deno run --allow-hrtime index.js
deno run --allow-hrtime index.js

伺服器端效能通常更容易評估和管理,因為它取決於負載、CPU、RAM、硬碟和雲服務限制。硬體升級或流程管理選項(例如PM2叢集Kubernetes)可能比重構程式碼更有效。

出於這個原因,以下部分將重點介紹客戶端效能。

自定義效能測量

Performance API可用於確定應用程式函式的執行速度。您可能使用過或遇到過使用Date()的計時函式:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const timeStart = new Date();
runMyCode();
const timeTaken = new Date() - timeStart;
console.log(`runMyCode() executed in ${ timeTaken }ms`);
const timeStart = new Date(); runMyCode(); const timeTaken = new Date() - timeStart; console.log(`runMyCode() executed in ${ timeTaken }ms`);
const timeStart = new Date();
runMyCode();
const timeTaken = new Date() - timeStart;
console.log(`runMyCode() executed in ${ timeTaken }ms`);

Performance API提供了兩個主要好處:

  1. 更好的準確性: Date()測量到最接近的毫秒,但Performance API可以測量毫秒的分數(取決於瀏覽器)。
  2. 更好的可靠性:使用者或作業系統可以更改系統時間,因此Date()基於度量的指標並不總是準確的。這意味著當時鍾向前移動時,您的函式可能會顯得特別慢!

Date()等效的方法是performance.now()返回一個高解析度時間戳,當負責建立文件的程序啟動時(頁面已載入),該時間戳設定為零:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const timeStart = performance.now();
runMyCode();
const timeTaken = performance.now() - timeStart;
console.log(`runMyCode() executed in ${ timeTaken }ms`);
const timeStart = performance.now(); runMyCode(); const timeTaken = performance.now() - timeStart; console.log(`runMyCode() executed in ${ timeTaken }ms`);
const timeStart = performance.now();
runMyCode();
const timeTaken = performance.now() - timeStart;
console.log(`runMyCode() executed in ${ timeTaken }ms`);

非標準performance.timeOrigin屬性也可以返回從1970年1月1日開始的時間戳,儘管這在IE和Deno中不可用。

performance.now()在進行多次測量時變得不切實際。Performance API提供了一個緩衝區,您可以在其中記錄事件以供以後分析,方法是將標籤名稱傳遞給performance.mark()

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
performance.mark('start:app');
performance.mark('start:init');
init(); // run initialization functions
performance.mark('end:init');
performance.mark('start:funcX');
funcX(); // run another function
performance.mark('end:funcX');
performance.mark('end:app');
performance.mark('start:app'); performance.mark('start:init'); init(); // run initialization functions performance.mark('end:init'); performance.mark('start:funcX'); funcX(); // run another function performance.mark('end:funcX'); performance.mark('end:app');
performance.mark('start:app');
performance.mark('start:init');
init(); // run initialization functions
performance.mark('end:init');
performance.mark('start:funcX');
funcX(); // run another function
performance.mark('end:funcX');
performance.mark('end:app');

可以使用以下方法提取效能緩衝區中所有標記物件的陣列:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const mark = performance.getEntriesByType('mark');
const mark = performance.getEntriesByType('mark');
const mark = performance.getEntriesByType('mark');

示例結果:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[
{
detail: null
duration: 0
entryType: "mark"
name: "start:app"
startTime: 1000
},
{
detail: null
duration: 0
entryType: "mark"
name: "start:init"
startTime: 1001
},
{
detail: null
duration: 0
entryType: "mark"
name: "end:init"
startTime: 1100
},
...
]
[ { detail: null duration: 0 entryType: "mark" name: "start:app" startTime: 1000 }, { detail: null duration: 0 entryType: "mark" name: "start:init" startTime: 1001 }, { detail: null duration: 0 entryType: "mark" name: "end:init" startTime: 1100 }, ... ]
[
{
detail: null
duration: 0
entryType: "mark"
name: "start:app"
startTime: 1000
},
{
detail: null
duration: 0
entryType: "mark"
name: "start:init"
startTime: 1001
},
{
detail: null
duration: 0
entryType: "mark"
name: "end:init"
startTime: 1100
},
...
]

performance.measure()方法計算兩個標記之間的時間並將其儲存在效能緩衝區中。您傳遞一個新的度量名稱、起始標記名稱(或null以從頁面載入測量)和結束標記名稱(或null以測量當前時間):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
performance.measure('init', 'start:init', 'end:init');
performance.measure('init', 'start:init', 'end:init');
performance.measure('init', 'start:init', 'end:init');

一個PerformanceMeasure物件被附加到具有計算持續時間的緩衝區中。要獲取此值,您可以請求所有度量的陣列:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const measure = performance.getEntriesByType('measure');
const measure = performance.getEntriesByType('measure');
const measure = performance.getEntriesByType('measure');

或按其名稱請求度量:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
performance.getEntriesByName('init');
performance.getEntriesByName('init');
performance.getEntriesByName('init');

示例結果:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[
{
detail: null
duration: 99
entryType: "measure"
name: "init"
startTime: 1001
}
]
[ { detail: null duration: 99 entryType: "measure" name: "init" startTime: 1001 } ]
[
{
detail: null
duration: 99
entryType: "measure"
name: "init"
startTime: 1001
}
]

使用效能緩衝器

除了標記和測量,效能緩衝區還用於自動記錄導航時間、資源時間和繪製時間(我們將在後面討論)。您可以獲得緩衝區中所有條目的陣列:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
performance.getEntries();
performance.getEntries();
performance.getEntries();

預設情況下,大多數瀏覽器提供一個緩衝區,最多可儲存150個資源指標。這對於大多數評估來說應該足夠了,但是如果需要,您可以增加或減少緩衝區限制:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// record 500 metrics
performance.setResourceTimingBufferSize(500);
// record 500 metrics performance.setResourceTimingBufferSize(500);
// record 500 metrics
performance.setResourceTimingBufferSize(500);

可以按名稱清除標記,也可以指定一個空值來清除所有標記:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
performance.clearMarks('start:init');
performance.clearMarks('start:init');
performance.clearMarks('start:init');

同樣,可以按名稱或空值清除所有度量值:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
performance.clearMeasures();
performance.clearMeasures();
performance.clearMeasures();

監控效能緩衝區更新

PerformanceObserver可以監視效能緩衝區的更改並在發生特定事件時執行函式。如果您使用MutationObserver來響應DOM更新或使用IntersectionObserver來檢測元素何時滾動到視口中,那麼語法將會很熟悉。

您必須使用兩個引數定義觀察者函式:

  1. 已檢測到的觀察者條目陣列,以及
  2. 觀察者物件。如有必要,disconnect()可以呼叫它的方法來停止觀察者。
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function performanceCallback(list, observer) {
list.getEntries().forEach(entry => {
console.log(`name : ${ entry.name }`);
console.log(`type : ${ entry.type }`);
console.log(`start : ${ entry.startTime }`);
console.log(`duration: ${ entry.duration }`);
});
}
function performanceCallback(list, observer) { list.getEntries().forEach(entry => { console.log(`name : ${ entry.name }`); console.log(`type : ${ entry.type }`); console.log(`start : ${ entry.startTime }`); console.log(`duration: ${ entry.duration }`); }); }
function performanceCallback(list, observer) {
list.getEntries().forEach(entry => {
console.log(`name    : ${ entry.name }`);
console.log(`type    : ${ entry.type }`);
console.log(`start   : ${ entry.startTime }`);
console.log(`duration: ${ entry.duration }`);
});
}

該函式被傳遞給一個新的 PerformanceObserver 物件。它的observe()方法被傳遞一個 Performance buffer entryTypes 陣列來觀察:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let observer = new PerformanceObserver( performanceCallback );
observer.observe({ entryTypes: ['mark', 'measure'] });
let observer = new PerformanceObserver( performanceCallback ); observer.observe({ entryTypes: ['mark', 'measure'] });
let observer = new PerformanceObserver( performanceCallback );
observer.observe({ entryTypes: ['mark', 'measure'] });

在此示例中,新增新標記或度量會執行該performanceCallback()函式。雖然它只在此處記錄訊息,但它可用於觸發資料上傳或進行進一步計算。

測量繪製效能

Paint Timing API僅在客戶端JavaScript中可用,並自動記錄對Core Web Vitals很重要的兩個指標:

  1. first-paint:瀏覽器已經開始繪製頁面。
  2. first-contentful-paint:瀏覽器繪製了DOM內容的第一個重要項,例如標題或影象。

這些可以從效能緩衝區中提取到一個陣列中:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const paintTimes = performance.getEntriesByType('paint');
const paintTimes = performance.getEntriesByType('paint');
const paintTimes = performance.getEntriesByType('paint');

在頁面完全載入之前,請小心執行此命令;值將不會準備好。要麼等window.load事件或使用PerformanceObserver監視paint入口型別。

示例結果:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[
{
"name": "first-paint",
"entryType": "paint",
"startTime": 812,
"duration": 0
},
{
"name": "first-contentful-paint",
"entryType": "paint",
"startTime": 856,
"duration": 0
}
]
[ { "name": "first-paint", "entryType": "paint", "startTime": 812, "duration": 0 }, { "name": "first-contentful-paint", "entryType": "paint", "startTime": 856, "duration": 0 } ]
[
{
"name": "first-paint",
"entryType": "paint",
"startTime": 812,
"duration": 0
},
{
"name": "first-contentful-paint",
"entryType": "paint",
"startTime": 856,
"duration": 0
}
]

緩慢的首次繪製通常是由阻止渲染的CSS或JavaScript引起的。如果瀏覽器必須下載大影象或渲染複雜元素,則與first-contentful-paint的差距可能會很大。

資源效能測量

影象、樣式表和JavaScript檔案等資源的網路計時會自動記錄到效能緩衝區。雖然您幾乎無法解決網路速度問題(除了減小檔案大小),但它可以幫助突出資產較大、Ajax響應緩慢或第三方指令碼效能不佳的問題。

可以使用以下方法從緩衝區中提取一組PerformanceResourceTiming指標

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const resources = performance.getEntriesByType('resource');
const resources = performance.getEntriesByType('resource');
const resources = performance.getEntriesByType('resource');

或者,您可以通過傳遞其完整URL來獲取資產的指標:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const resource = performance.getEntriesByName('https://test.com/script.js');
const resource = performance.getEntriesByName('https://test.com/script.js');
const resource = performance.getEntriesByName('https://test.com/script.js');

示例結果:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
[
{
connectEnd: 195,
connectStart: 195,
decodedBodySize: 0,
domainLookupEnd: 195,
domainLookupStart: 195,
duration: 2,
encodedBodySize: 0,
entryType: "resource",
fetchStart: 195,
initiatorType: "script",
name: "https://test.com/script.js",
nextHopProtocol: "h3",
redirectEnd: 0,
redirectStart: 0,
requestStart: 195,
responseEnd: 197,
responseStart: 197,
secureConnectionStart: 195,
serverTiming: [],
startTime: 195,
transferSize: 0,
workerStart: 195
}
]
[ { connectEnd: 195, connectStart: 195, decodedBodySize: 0, domainLookupEnd: 195, domainLookupStart: 195, duration: 2, encodedBodySize: 0, entryType: "resource", fetchStart: 195, initiatorType: "script", name: "https://test.com/script.js", nextHopProtocol: "h3", redirectEnd: 0, redirectStart: 0, requestStart: 195, responseEnd: 197, responseStart: 197, secureConnectionStart: 195, serverTiming: [], startTime: 195, transferSize: 0, workerStart: 195 } ]
[
{
connectEnd: 195,
connectStart: 195,
decodedBodySize: 0,
domainLookupEnd: 195,
domainLookupStart: 195,
duration: 2,
encodedBodySize: 0,
entryType: "resource",
fetchStart: 195,
initiatorType: "script",
name: "https://test.com/script.js",
nextHopProtocol: "h3",
redirectEnd: 0,
redirectStart: 0,
requestStart: 195,
responseEnd: 197,
responseStart: 197,
secureConnectionStart: 195,
serverTiming: [],
startTime: 195,
transferSize: 0,
workerStart: 195
}
]

可以檢查以下屬性:

  • name:資源網址
  • entryType:“資源”
  • initiatorType: 資源是如何被啟動的,例如“指令碼”或“連結”
  • serverTimingPerformanceServerTiming伺服器在HTTP Server-Timing標頭中傳遞的物件陣列(您的伺服器端應用程式可以將指標傳送到客戶端以進行進一步分析)
  • startTime:獲取開始時的時間戳
  • nextHopProtocol : 使用的網路協議
  • workerStart:啟動Progressive Web App Service Worker之前的時間戳(如果請求未被Service Worker攔截,則為 0)
  • redirectStart:重定向開始時的時間戳
  • redirectEnd : 最後一個重定向響應的最後一個位元組之後的時間戳
  • fetchStart:資源獲取之前的時間戳
  • domainLookupStart : DNS查詢前的時間戳
  • domainLookupEnd : DNS查詢後的時間戳
  • connectStart:建立伺服器連線之前的時間戳
  • connectEnd : 建立伺服器連線後的時間戳
  • secureConnectionStart : SSL握手之前的時間戳
  • requestStart : 瀏覽器請求資源之前的時間戳
  • responseStart : 瀏覽器接收到第一個位元組資料的時間戳
  • responseEnd : 收到最後一個位元組或關閉連線後的時間戳
  • duration : startTime和responseEnd之間的差異
  • transferSize:以位元組為單位的資源大小,包括標頭和壓縮主體
  • encodeBodySize : 解壓縮前的資源體(以位元組為單位)
  • decodedBodySize : 解壓後的資源體,以位元組為單位

此示例指令碼檢索由Fetch API發起的所有Ajax請求並返回總傳輸大小和持續時間:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const fetchAll = performance.getEntriesByType('resource')
.filter( r => r.initiatorType === 'fetch')
.reduce( (sum, current) => {
return {
transferSize: sum.transferSize += current.transferSize,
duration: sum.duration += current.duration
}
},
{ transferSize: 0, duration: 0 }
);
const fetchAll = performance.getEntriesByType('resource') .filter( r => r.initiatorType === 'fetch') .reduce( (sum, current) => { return { transferSize: sum.transferSize += current.transferSize, duration: sum.duration += current.duration } }, { transferSize: 0, duration: 0 } );
const fetchAll = performance.getEntriesByType('resource')
.filter( r => r.initiatorType === 'fetch')
.reduce( (sum, current) => {
return {
transferSize: sum.transferSize += current.transferSize,
duration: sum.duration += current.duration
}
},
{ transferSize: 0, duration: 0 }
);

解除安裝前一頁和載入當前頁的網路時間會作為單個PerformanceNavigationTiming物件自動記錄到效能緩衝區。

使用以下方法將其提取到陣列中:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const pageTime = performance.getEntriesByType('navigation');
const pageTime = performance.getEntriesByType('navigation');
const pageTime = performance.getEntriesByType('navigation');

…或者通過將頁面URL傳遞給.getEntriesByName()

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const pageTiming = performance.getEntriesByName(window.location);
const pageTiming = performance.getEntriesByName(window.location);
const pageTiming = performance.getEntriesByName(window.location);

這些指標與資源的指標相同,但還包括特定於頁面的值:

  • entryType : 例如“導航”
  • type:“navigate”、“reload”、“back_forward”或“prerender”
  • redirectCount : 重定向次數
  • unloadEventStart : 上一個文件的解除安裝事件之前的時間戳
  • unloadEventEnd : 上一個文件的解除安裝事件之後的時間戳
  • domInteractive : 瀏覽器解析HTML並構建DOM的時間戳
  • domContentLoadedEventStart:文件的DOMContentLoaded事件觸發之前的時間戳
  • domContentLoadedEventEnd : 文件的DOMContentLoaded事件完成後的時間戳
  • domComplete : DOM構建和DOMContentLoaded事件完成後的時間戳
  • loadEventStart:頁面載入事件觸發之前的時間戳
  • loadEventEnd : 頁面載入事件和所有資產可用後的時間戳

典型問題包括:

  • unloadEventEnddomInteractive之間有很長的延遲。這可能表明伺服器響應緩慢。
  • domContentLoadedEventStartdomComplete之間有很長的延遲。這可能表明頁面啟動指令碼太慢了。
  • domCompleteloadEventEnd之間有很長的延遲。這可能表明該頁面有太多資源,或者有幾個資源載入時間過長。

效能記錄與分析

Performance API允許您整理真實世界的使用資料並將其上傳到伺服器以進行進一步分析。您可以使用第三方服務(例如Google Analytics)來儲存資料,但第三方指令碼可能會被阻止或引入新的效能問題。您可以根據自己的要求定製您自己的解決方案,以確保監控不會影響其他功能。

警惕無法確定統計資料的情況——可能是因為使用者使用舊瀏覽器、攔截JavaScript或在公司代理後面。瞭解缺少哪些資料可能比基於不完整資訊做出假設更有成效。

理想情況下,您的分析指令碼不會因執行復雜計算或上傳大量資料而對效能產生負面影響。考慮利用網路工作者並儘量減少同步localStorage呼叫的使用。以後總是可以批量處理原始資料。

最後,要警惕異常值,例如對統計資料產生不利影響的非常快或非常慢的裝置和連線。例如,如果9個使用者在2秒內載入了一個頁面,但第10個使用者的下載時間為60秒,則平均延遲接近8秒。更現實的指標是中位數(2秒)或第90個百分位(每10個使用者中有9個使用者的載入時間為2秒或更短)。

小結

Web效能仍然是開發人員的一個關鍵因素。使用者希望網站和應用程式能夠在大多數裝置上做出響應。搜尋引擎優化也會受到影響,因為在Google中速度較慢的網站會被降級

有很多效能監控工具,但大多數評估伺服器端執行速度或使用有限數量的有能力的客戶端來判斷瀏覽器渲染。Performance API提供了一種整理真實使用者指標的方法,這是任何其他方式都無法計算的。

評論留言