Performance API不完全使用指北

2023-02-07 06:00:35

本教學解釋瞭如何使用Performance API來記錄真實使用者存取你的應用程式的統計資料。

使用瀏覽器的DevTools來評估web應用效能是很有用的,但要復現現實世界的使用情況並不容易。因為人們在不同地點使用不同的裝置、瀏覽器和網路,都會有不同的體驗。

Performance API介紹

Performance API使用一個緩衝區,在你的網頁生命週期的確定節點上,在物件屬性中記錄類似DevTool的指標。這些節點包括:

  1. 頁面導航:記錄頁面載入重定向、連線、握手、DOM事件等等。
  2. 資源載入:記錄資源載入,比如影象、CSS、指令碼以及Ajax呼叫。
  3. 繪製指標:記錄瀏覽器渲染資訊。
  4. 自定義:記錄任意的應用處理時間,來找到執行慢的函數。

所有的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物件陣列

注意:performanceServerTimingnamedescriptionduration等指標由伺服器響應寫入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 hooksperf_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');

陣列裡的物件擁有entryTypenamestartTime屬性:

[
  {
    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');

其他方法:

PerformanceObserver可以監聽緩衝區的更改,當指定物件出現時執行函數。觀察者函數使用兩個引數定義:

  1. list:觀察者條目
  2. 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(一般來說是markmeasure或者resource):

let observer = new PerformanceObserver(performanceHandler);
observer.observe({entryTypes: ['mark', 'measure']});

每當有新的標記或測量物件被推播到效能緩衝區,performanceHandler()函數就會執行。

總結

Performance API提供了一種方法來測量網站和應用程式的速度,這些裝置是由不同地點的人在一系列連線上使用的實際裝置。它使每個人都能輕鬆地整理出類似DevTool的指標,並識別潛在的瓶頸。

以上就是本文的全部內容,如果對你有所幫助,歡迎點贊、收藏、轉發~