以使用者為中心的效能指標【譯】

antwang發表於2018-05-23

你可能不止一次地聽大家討論效能的話題、一個速度飛快的web 應用是多麼重要。

我的網站快嗎?當你試圖回答這個問題的時候,你會發現快是個很模糊的概念。我們在說快的時候,我們到底指的哪些方面?是在什麼場景下?對誰而言?

談論效能的時候務必要準確,不要使用錯誤的概念,以免開發人員一直在錯誤的事情上做優化——結果沒有得到優化反而損害到使用者體驗。

看一個具體的例子,我們經常會聽到有人說:我的app測過了,載入時間是XX秒。

上述的說法並不是說它是錯誤的,而是它歪曲了現實。載入時間因使用者而異,取決於他們的裝置能力和網路環境。將只關注載入時間單一資料指標會遺漏那些載入時間長的使用者。

實際上,上面所說的載入時間是一個所有使用者的一個平均載入時間。只有下圖所示的直方圖分佈圖才能完全反應真實的載入時間:

以使用者為中心的效能指標【譯】

X軸上的數字表示載入時間,y軸直方圖的高度表示某個時間內的使用者數量。如圖所示,雖然大多數使用者的載入時間在1~2秒,但仍有不少使用者的載入時間很長。

"我的頁面載入時間是xx秒"不真實的另一個原因是,頁面的載入不是單一的一個時間指標——它是使用者的使用體驗,而這種體驗是沒有任何一個指標能完全捕獲到的。載入過程中有多個時間指標會影響使用者對頁面載入是否足夠快的感受,如果你只盯著載入完成時間這一個指標,那麼你可能會忽視發生在其他時間點的不佳使用者體驗。

例如,一個應用的初始渲染優化的非常快,頁面內容很快就展示給使用者了。如果這個應用隨後載入了一個很大的js包,並且需要耗費好幾秒解析和執行,頁面內容在js執行完成之前,還是沒法響應使用者的操作。如果使用者看到一個連結卻無法點選,有了文字框卻沒法輸入,他們或許不在乎你的頁面渲染有多快的。

所以不能使用單一的指標來衡量載入的速度,我們應該關注整個載入過程中的任何影響使用者感受的時間指標。

第二個誤區是,認為效能只是在載入時需要關注的問題。

我們作為一個團隊在這方面犯了錯誤,並且由於多數效能檢測工具只檢測載入效能,這個錯誤還被放大了。

事實上效能問題可能在任何時間發生,不只是在載入的時候。使用者的點選響應速度慢,頁面不能滾動,動畫不流暢同樣影響體驗。使用者關心的是整體的體驗,作為開發者我們也應如此。

這些誤區的共同點是,我們關注的指標跟使用者體驗沒有關係或者說關係很小。同樣,傳統的效能指標如頁面載入時間,DOMContentLoaded時間是非常不可靠的。因為,當他們出現時,並不等於使用者認為應用已經載入完成了。

所有為確保不重複這樣的錯誤,我們問自己幾個問題:

  • 什麼指標最準確地反映使用者直觀體驗?
  • 如何在真實使用者中檢測這些指標?
  • 如何分享得到的資料以衡量應用是否夠快?
  • 理解了什麼是應用的真實使用者效能,我們如何防止效能退化?未來如何進行優化?

使用者為中心的效能指標

當使用者訪問一個頁面的時候,通常會從視覺上去感知是不是頁面已經如預期地載入完成可以正常使用了。

主題 說明
發生了嗎? 頁面是否開始跳轉?伺服器有沒有響應?
內容重要嗎? 重要的內容是否已經渲染?
可以使用嗎? 頁面可互動了嗎?或者還在載入中嗎?
體驗好嗎? 互動是否平滑自然,沒有卡頓?

為了瞭解頁面在使用者側的在這些方面的表現,我們定義了一些指標:

首次繪製和首次有內容的繪製

Paint Timing 介面定義了兩個指標:首次繪製(FP)和首次內容繪製(FCP)。這些指標記錄了瀏覽器開始在螢幕上進行繪製的時間點。這對使用者很重要,因為它回答了:”發生了嗎?”這個問題。

這兩個指標的主要區別是FP是頁面在視覺上首次出現不同於跳轉前的內容的時間點。相比之下,FCP是瀏覽器渲染DOM中第一個內容的時候,可能是文字,影象,SVG甚至是<canvas>元素。

首次有意義繪製和關鍵元素渲染時間

首次有用的繪製回答了這個問題:“它有用嗎?”。“有用”這個概念沒有一個標準的定義。但是對於開發者來說,找出頁面上的哪些部分對使用者是最重要的是很容易的。

以使用者為中心的效能指標【譯】
這些網頁的“最重要的部分”通常稱為關鍵元素。例如,在YouTube觀看頁面上,關鍵元素是主視訊。 在Twitter上,它可能是通知徽章和第一條推文。在天氣應用中,是指定城市的天氣預測。 而在新聞網站上,它可能是主要故事和精選圖片。

網頁上幾乎總是有比其他內容更重要的部分。如果網頁中最重要的部分載入速度很快,使用者可能甚至不會注意到頁面的其他部分是否沒有載入完成。

長任務

瀏覽器通過向主執行緒上的佇列新增任務並逐一執行來響應使用者輸入。這也是瀏覽器執行JavaScript的地方,所以在這個意義上說瀏覽器是單執行緒的。

在某些情況下,這些任務可能需要很長時間才能執行完,這樣的話主執行緒將被阻塞,並且佇列中的所有其他任務都必須等待。

以使用者為中心的效能指標【譯】
對使用者而言這表現為卡頓不流暢,這也是當前頁面效能差的主要原因。

長任務API能識別任何長於50毫秒的任務,它認為這存在效能隱患。通過長任務API,開發者能獲取到頁面中存在的長任務。選擇50ms是為了遵循RAIL指南以確保100ms內響應使用者的輸入。

可互動時間

可互動時間(TTI)意味著頁面渲染完成並且可以正常響應使用者的輸入了,可能有以下幾個原因導致頁面不能響應使用者輸入:

  • 確保頁面可互動的js沒有下載完成
  • 存在長任務(上節所述)

TTI表示頁面的初始JavaScript載入完成且主執行緒空閒(沒有長任務)的點。

將指標關聯到使用者體驗

回到我們以前認為對使用者體驗最重要的問題,本表概述了我們剛剛列出的每個指標如何對映到我們希望優化的使用者體驗:

體驗 指標
發生了嗎? 首次繪製(FP) / 首次內容繪製 (FCP)
內容重要嗎? 首次有用繪製 (FMP) / 關鍵元素渲染時間
可以使用嗎? 可互動時間(TTI)
體驗好嗎? 長任務

頁面載入時間線的截圖可以幫助你更好地確認這些指標處於載入過程的什麼位置。

以使用者為中心的效能指標【譯】
下一節將詳細介紹如何在真實使用者的裝置上測量這些指標。

在真實使用者的裝置上衡量這些指標

我們歷來為loadDOMContentLoaded等指標進行優化的主要原因之一是,它們作為瀏覽器中的事件,易於在真實使用者上進行測量。

相比之下,很多其他指標歷來很難測量。例如,我們經常看到開發人員用這段折中的程式碼來測量長任務:

(function detectLongFrame() {
  var lastFrameTime = Date.now();
  requestAnimationFrame(function() {
    var currentFrameTime = Date.now();

    if (currentFrameTime - lastFrameTime > 50) {
      // Report long frame here...
    }

    detectLongFrame(currentFrameTime);
  });
}());
複製程式碼

此程式碼使用requestAnimationFrame迴圈記錄每次迭代的時間。如果當前時間比前一次超過50毫秒,則認為這是長任務。 雖然這些程式碼起作用,但它有很多缺點:

  • 增加了每一幀的開銷。
  • 阻止空閒時間塊的出現。
  • 影響電池壽命。 效能檢測程式碼最重要的原則是不能使效能變得更差。

LighthouseWeb Page Test雖然提供這些新的效能指標已經有一段時間了(他們是專案釋出前進行效能測試的絕佳工具),但是畢竟他們不是執行在使用者裝置上,還是沒辦法衡量web專案在使用者裝置上的實際效能表現。

幸運的是,瀏覽器提供了一些新API,這些新API使得統計真實使用者裝置的效能指標變得很簡單,不需要再使用一些影響頁面效能的變通方法。

這些新的API是PerformanceObserverPerformanceEntryDOMHighResTimeStamp。接下來我們通過一個例子,來了解一下怎麼通過PerformanceObserver來統計繪製相關的效能(例如,FP,FCP)以及可能出現的導致頁面阻塞的js長任務。

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // `entry` is a PerformanceEntry instance.
    console.log(entry.entryType);
    console.log(entry.startTime); // DOMHighResTimeStamp
    console.log(entry.duration); // DOMHighResTimeStamp
  }
});

// Start observing the entry types you care about.
observer.observe({entryTypes: ['resource', 'paint']});
複製程式碼

通過PerformanceObserver我們可以訂閱效能事件,當事件發生的時候得到相應的資料。相比老的PerformanceTiming 介面,它的好處是以非同步的方式獲取資料,而不是通過不斷的輪詢。

統計FP / FCP

獲取到某個效能資料後,可以將該使用者的裝置的效能資料傳送到任意的資料分析服務。比如我們將首次繪製的指標傳送到谷歌統計。

<head>
  <!-- Add the async Google Analytics snippet first. -->
  <script>
  window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
  ga('create', 'UA-XXXXX-Y', 'auto');
  ga('send', 'pageview');
  </script>
  <script async src='https://www.google-analytics.com/analytics.js'></script>

  <!-- Register the PerformanceObserver to track paint timing. -->
  <script>
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      // `name` will be either 'first-paint' or 'first-contentful-paint'.
      const metricName = entry.name;
      const time = Math.round(entry.startTime + entry.duration);

      ga('send', 'event', {
        eventCategory: 'Performance Metrics',
        eventAction: metricName,
        eventValue: time,
        nonInteraction: true,
      });
    }
  });
  observer.observe({entryTypes: ['paint']});
  </script>

  <!-- Include any stylesheets after creating the PerformanceObserver. -->
  <link rel="stylesheet" href="...">
</head>
複製程式碼

基於關鍵關鍵元素統計FMP

我們還沒有FMP的標準化定義(因此也沒有對應的效能型別)。這部分是因為很難有一個通用的指標來表示所有頁面是“有意義的”。

但是,在單頁面應用的場景下,我們可以用關鍵元素的顯示的時間點來表示FMP。

Steve Souders有一篇名為User Timing And Custom Metrics的精彩文章,詳細介紹了許多使用瀏覽器效能API確定何時可以看到各種型別媒體的技術。

統計 TTI

從長遠來看,我們希望通過PerformanceObserver在瀏覽器中對TTI指標提供標準化的支援。 與此同時,我們開發了一種可用於檢測TTI的polyfill,並可在任何支援長任務 API的瀏覽器中工作。

這個polyfill暴露了一個getFirstConsistentlyInteractive()方法,該方法返回一個以TTI值解析的promise物件。 你可以使用Google Analytics統計TTI,如下所示:

import ttiPolyfill from './path/to/tti-polyfill.js';

ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
  ga('send', 'event', {
    eventCategory: 'Performance Metrics',
    eventAction: 'TTI',
    eventValue: tti,
    nonInteraction: true,
  });
});
複製程式碼

getFirstConsistentlyInteractive()方法接受一個可選的startTime配置選項,用以指定一個時間表示web應用在此時間以前不能進行互動。預設情況下,polyfill使用DOMContentLoaded作為開始時間,但使用類似於關鍵元素可見的時刻或當獲知已新增所有事件偵聽器時的時刻,通常會更準確。

完整的安裝和使用說明,請參閱TTI polyfill文件。

統計長任務

我前面提到長任務會導致一些負面的使用者體驗(例如,緩慢的事件處理函式,丟幀)。我們最好留意一下長任務發生的頻率,以將其影響最小化。

要在JavaScript中檢測長任務,請建立一個PerformanceObserver物件並觀察 longtask型別。長任務型別的一個優點是它包含一個attribution屬性,因此可以更輕鬆地追蹤哪些程式碼導致了長任務:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    ga('send', 'event', {
      eventCategory: 'Performance Metrics',
      eventAction: 'longtask',
      eventValue: Math.round(entry.startTime + entry.duration),
      eventLabel: JSON.stringify(entry.attribution),
    });
  }
});

observer.observe({entryTypes: ['longtask']});
複製程式碼

attribution屬性會告訴你什麼程式碼導致了長任務,這有助於確定第三方iframe指令碼是否導致問題。該規範未來版本正計劃新增更多粒度,並提供指令碼URL,行和列號,這對確定自己的指令碼是否導致緩慢很有幫助。

統計輸入延遲

阻塞主執行緒的長任務會阻止您的事件偵聽器及時執行。RAIL效能模型告訴我們,為了使使用者介面感覺平滑,應該在使用者輸入的100毫秒內做出響應,否則,應該分析是什麼原因。

要檢測程式碼中的輸入延遲,可以將事件的時間戳與當前時間進行比較,如果差異大於100毫秒,則可以(也應該)上報異常。

const subscribeBtn = document.querySelector('#subscribe');

subscribeBtn.addEventListener('click', (event) => {
  // Event listener logic goes here...

  const lag = performance.now() - event.timeStamp;
  if (lag > 100) {
    ga('send', 'event', {
      eventCategory: 'Performance Metric'
      eventAction: 'input-latency',
      eventLabel: '#subscribe:click',
      eventValue: Math.round(lag),
      nonInteraction: true,
    });
  }
});
複製程式碼

由於事件延遲通常是長任務的結果,因此你可以將事件延遲檢測邏輯與長任務檢測邏輯相結合:如果長任務與event.timeStamp同時阻塞主執行緒,則也可以上報該長任務 attribution值。 這可以確定導致效能體驗差的的程式碼是什麼。

雖然這種技術並不完美(它在冒泡階段不處理長事件監聽器,並且它不適用於不在主執行緒上執行的滾動或合成動畫),但卻是更好地理解長時間執行的JavaScript程式碼會影響使用者體驗的第一步。

資料解釋

一旦開始收集真實使用者的效能指標,你需要將這些資料付諸實踐。真實使用者效能資料之所以重要主要是由於以下幾個原因:

  • 驗證你的應用是否按預期執行。
  • 找出效能差對轉化率的影響(無論轉化率對你的應用而言意味著什麼)。
  • 尋求改善使用者體驗的措施。 你的應用在移動裝置和桌面裝置上的表現絕對是值得比較的一件事。下圖顯示了桌面(藍色)和移動(橙色)的TTI分佈。從這個例子可以看出,手機上的TTI值比桌面上的要長很多:

以使用者為中心的效能指標【譯】

雖然這裡的資料是特定於應用的(你應該自己測試一下自己應用的資料),下面的例子是一個基於效能指標生成的分析報告:

桌面端

Percentile TTI (seconds)
50% 2.3
75% 4.7
90% 8.3

移動端

Percentile TTI (seconds)
50% 3.9
75% 8.0
90% 12.6

通過將資料分解成移動和桌面,並將各個終端的資料採用分佈圖展示,可以快速洞察真實使用者的體驗。 例如,看上面的表格,可以很容易看到對於這個應用,10%的移動使用者花費了超過12秒的時間來互動!

效能如何影響業務

載入過程使用者跳出

我們知道,如果頁面載入時間過長,使用者通常會離開。這意味著我們所有的效能指標都存在生存偏差的問題,其中的資料並不包括那些沒有等待頁面完成載入的使用者的指標。

雖然不能獲取這些使用者滯留的資料,但可以獲取這種情況發生的頻率以及每個使用者停留的時間。

這對於使用Google Analytics來說有點棘手,因為analytics.js庫通常是非同步載入的,並且在使用者決定離開時可能不可用。 不過,在向Google Analytics傳送資料之前,您無需等待analytics.js載入。 您可以通過Measurement Protocol直接傳送它。

此程式碼監聽一個visibilitychange事件(如果當前頁面進入後臺執行或者頁面關閉觸發該事件),當事件觸發時傳送performance.now()的值。

<script>
window.__trackAbandons = () => {
  // Remove the listener so it only runs once.
  document.removeEventListener('visibilitychange', window.__trackAbandons);
  const ANALYTICS_URL = 'https://www.google-analytics.com/collect';
  const GA_COOKIE = document.cookie.replace(
    /(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1');
  const TRACKING_ID = 'UA-XXXXX-Y';
  const CLIENT_ID =  GA_COOKIE || (Math.random() * Math.pow(2, 52));

  // Send the data to Google Analytics via the Measurement Protocol.
  navigator.sendBeacon && navigator.sendBeacon(ANALYTICS_URL, [
    'v=1', 't=event', 'ec=Load', 'ea=abandon', 'ni=1',
    'dl=' + encodeURIComponent(location.href),
    'dt=' + encodeURIComponent(document.title),
    'tid=' + TRACKING_ID,
    'cid=' + CLIENT_ID,
    'ev=' + Math.round(performance.now()),
  ].join('&'));
};
document.addEventListener('visibilitychange', window.__trackAbandons);
</script>
複製程式碼

你可以將此程式碼複製到文件的<head>中,並使用你的track ID替換UA-XXXXX-Y佔位符。

你還需要確保在頁面變為可互動時刪除此監聽器,否則你上報TTI的時候會誤將放棄載入等待業上報。

document.removeEventListener('visibilitychange', window.__trackAbandons);
複製程式碼

效能優化和防效能退化

定義以使用者為中心的指標的好處是,當針對它們進行優化時,必然會促進使用者體驗的提升。

提高效能的最簡單方法之一就是隻向客戶端傳送較少的JavaScript程式碼,但在不能減少程式碼大小的情況下,關鍵是要考慮如何交付JavaScript

優化 FP/FCP

你可以通過從文件的<head>中刪除任何阻塞渲染的指令碼或樣式表來縮短首次繪製和首次內容繪製的時間。

花時間確定使用者感知"it's happening"所需的最小樣式集,並將其內聯到<head>中,(或者通過HTTP2服務推送),你將獲得難以置信的快速首次繪製時間。

PWA中應用的app shell 模式就是一個應用典範。

優化 FMP/TTI

一旦確定了頁面上最關鍵的UI元素,你應該確保載入的初始指令碼僅包含使這些元素正常渲染和互動的程式碼。

任何與關鍵元素無關的程式碼包含在初始js模組中都會拖慢可互動時間。我們沒有理由強制使用者下載和解析暫時不需要的js程式碼。

通用的做法是,你應該儘可能的壓縮FMP和TTI之間的時間間隔。如果不能壓縮的話,清楚地提示使用者當前使用者還不能互動是很必要的。

最讓使用者煩躁的體驗就是點選一個元素,然而什麼也沒發生。

防止長任務

js程式碼分割,優化js的載入順序,不僅可以讓頁面可互動時間變快,還能減少長任務,減少由於長任務導致的輸入延遲和慢幀。

除了將程式碼拆分為單獨的檔案之外,還可以將同步的大程式碼塊拆分為非同步執行的小程式碼塊,或者推遲到下一個空閒點。通過以較小程式碼塊的方式非同步執行該邏輯,你可以在主執行緒上留出空間,讓瀏覽器響應使用者輸入。

最後,應該確保引用的第三方程式碼進行了長任務相關的測試。導致大量長任務的第三方廣告或者統計指令碼最終會損害你的業務。

防止效能退化

本文主要關注真實使用者的效能測量,雖然真實使用者資料是最終關注的效能資料,但測試環境資料對於確保您的應用在釋出新功能之前表現良好(並且不會退化)至關重要。測試階段對於退化檢測非常理想,因為它們在受控環境下執行,並且不易受真實使用者環境的隨機變異性影響。

LighthouseWeb Page Test這樣的工具可以整合到持續整合伺服器中,並且如果關鍵指標退化或下降到特定閾值以下,可以讓構建失敗。

對於已經發布的程式碼,可以新增自定義警報,當效能指標變差時及時通知你。例如,如果第三方釋出了新程式碼,並且你的使用者突然出現了很多的長任務,會警報通知你。

要成功防止效能退化,你需要在每個新功能版本中,都進行測試和真實使用者環境下的效能測試。

以使用者為中心的效能指標【譯】

總結和展望

去年,我們在瀏覽器上向開發人員開放以使用者為中心的指標方面取得了重大進展,但還沒有完成,並且還有更多已規劃的事情要做。

我們非常希望將可互動時間和關鍵元素顯示時間統計標準化,因此開發人員無需自己計算這些內容,也不需要依賴polyfills去實現。我們還希望讓開發人員更容易定位導致丟幀和輸入延遲的長任務和具體的程式碼位置。

雖然我們有更多的工作要做,但我們對取得的進展感到興奮。有了像PerformanceObserver這樣的新API以及瀏覽器本身支援的長任務,開發人員可以使用js原生的API來測量真實使用者的效能而不會降低使用者體驗。

最重要的指標是那些代表真實使用者體驗的指標,我們希望開發人員儘可能輕鬆地使使用者滿意並建立出色的應用程式。

原文地址

相關文章