博客 / 詳情

返回

Performance API不完全使用指北

本教程解釋瞭如何使用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 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');

數組裏的對象擁有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');

其他方法:

  • .getEntries():返回所有性能條目的數組。
  • [.clearMarks([name])](https://developer.mozilla.org...):清除指定名稱的標記(不指定名稱則清除所有標記)。
  • [.clearMeasures([name])](https://developer.mozilla.org...):清除指定名稱的測量(不指定名稱則清除所有測量)。

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的指標,並識別潛在的瓶頸。

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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.