你可能不止一次地聽你們討論性能的話題、一個速度飛快的web 應用是多麼重要。web
個人網站快嗎?當你試圖回答這個問題的時候,你會發現快是個很模糊的概念。咱們在說快的時候,咱們到底指的哪些方面?是在什麼場景下?對誰而言?shell
談論性能的時候務必要準確,不要使用錯誤的概念,以避免開發人員一直在錯誤的事情上作優化——結果沒有獲得優化反而損害到用戶體驗。canvas
看一個具體的例子,咱們常常會聽到有人說:個人app測過了,加載時間是XX秒。promise
上述的說法並非說它是錯誤的,而是它歪曲了現實。加載時間因用戶而異,取決於他們的設備能力和網絡環境。將只關注加載時間單一數據指標會遺漏那些加載時間長的用戶。瀏覽器
實際上,上面所說的加載時間是一個全部用戶的一個平均加載時間。只有下圖所示的直方圖分佈圖才能徹底反應真實的加載時間:性能優化
X軸上的數字表示加載時間,y軸直方圖的高度表示某個時間內的用戶數量。如圖所示,雖然大多數用戶的加載時間在1~2秒,但仍有很多用戶的加載時間很長。bash
"個人頁面加載時間是xx秒"不真實的另外一個緣由是,頁面的加載不是單一的一個時間指標——它是用戶的使用體驗,而這種體驗是沒有任何一個指標能徹底捕獲到的。加載過程當中有多個時間指標會影響用戶對頁面加載是否足夠快的感覺,若是你只盯着加載完成時間這一個指標,那麼你可能會忽視發生在其餘時間點的不佳用戶體驗。服務器
例如,一個應用的初始渲染優化的很是快,頁面內容很快就展現給用戶了。若是這個應用隨後加載了一個很大的js包,而且須要耗費好幾秒解析和執行,頁面內容在js執行完成以前,仍是無法響應用戶的操做。若是用戶看到一個連接卻沒法點擊,有了文本框卻無法輸入,他們或許不在意你的頁面渲染有多快的。cookie
因此不能使用單一的指標來衡量加載的速度,咱們應該關注整個加載過程當中的任何影響用戶感覺的時間指標。網絡
第二個誤區是,認爲性能只是在加載時須要關注的問題。
咱們做爲一個團隊在這方面犯了錯誤,而且因爲多數性能檢測工具只檢測加載性能,這個錯誤還被放大了。
事實上性能問題可能在任什麼時候間發生,不僅是在加載的時候。用戶的點擊響應速度慢,頁面不能滾動,動畫不流暢一樣影響體驗。用戶關心的是總體的體驗,做爲開發者咱們也應如此。
這些誤區的共同點是,咱們關注的指標跟用戶體驗沒有關係或者說關係很小。一樣,傳統的性能指標如頁面加載時間,DOMContentLoaded
時間是很是不可靠的。由於,當他們出現時,並不等於用戶認爲應用已經加載完成了。
全部爲確保不重複這樣的錯誤,咱們問本身幾個問題:
當用戶訪問一個頁面的時候,一般會從視覺上去感知是否是頁面已經如預期地加載完成能夠正常使用了。
主題 | 說明 |
---|---|
發生了嗎? | 頁面是否開始跳轉?服務器有沒有響應? |
內容重要嗎? | 重要的內容是否已經渲染? |
可使用嗎? | 頁面可交互了嗎?或者還在加載中嗎? |
體驗好嗎? | 交互是否平滑天然,沒有卡頓? |
爲了瞭解頁面在用戶側的在這些方面的表現,咱們定義了一些指標:
Paint Timing
接口定義了兩個指標:首次繪製(FP)和首次內容繪製(FCP)。這些指標記錄了瀏覽器開始在屏幕上進行繪製的時間點。這對用戶很重要,由於它回答了:」發生了嗎?」這個問題。
這兩個指標的主要區別是FP是頁面在視覺上首次出現不一樣於跳轉前的內容的時間點。相比之下,FCP是瀏覽器渲染DOM中第一個內容的時候,多是文本,圖像,SVG甚至是<canvas>
元素。
首次有用的繪製回答了這個問題:「它有用嗎?」。「有用」這個概念沒有一個標準的定義。可是對於開發者來講,找出頁面上的哪些部分對用戶是最重要的是很容易的。
這些網頁的「最重要的部分」一般稱爲關鍵元素。例如,在YouTube觀看頁面上,關鍵元素是主視頻。 在Twitter上,它多是通知徽章和第一條推文。在天氣應用中,是指定城市的天氣預測。 而在新聞網站上,它多是主要故事和精選圖片。網頁上幾乎老是有比其餘內容更重要的部分。若是網頁中最重要的部分加載速度很快,用戶可能甚至不會注意到頁面的其餘部分是否沒有加載完成。
瀏覽器經過向主線程上的隊列添加任務並逐一執行來響應用戶輸入。這也是瀏覽器執行JavaScript的地方,因此在這個意義上說瀏覽器是單線程的。
在某些狀況下,這些任務可能須要很長時間才能運行完,這樣的話主線程將被阻塞,而且隊列中的全部其餘任務都必須等待。
對用戶而言這表現爲卡頓不流暢,這也是當前頁面性能差的主要緣由。長任務API能識別任何長於50毫秒的任務,它認爲這存在性能隱患。經過長任務API,開發者能獲取到頁面中存在的長任務。選擇50ms是爲了遵循RAIL指南以確保100ms內響應用戶的輸入。
可交互時間(TTI)意味着頁面渲染完成而且能夠正常響應用戶的輸入了,可能有如下幾個緣由致使頁面不能響應用戶輸入:
TTI表示頁面的初始JavaScript加載完成且主線程空閒(沒有長任務)的點。
回到咱們之前認爲對用戶體驗最重要的問題,本表概述了咱們剛剛列出的每一個指標如何映射到咱們但願優化的用戶體驗:
體驗 | 指標 |
---|---|
發生了嗎? | 首次繪製(FP) / 首次內容繪製 (FCP) |
內容重要嗎? | 首次有用繪製 (FMP) / 關鍵元素渲染時間 |
可使用嗎? | 可交互時間(TTI) |
體驗好嗎? | 長任務 |
頁面加載時間線的截圖能夠幫助你更好地確認這些指標處於加載過程的什麼位置。
下一節將詳細介紹如何在真實用戶的設備上測量這些指標。咱們從來爲load
和DOMContentLoaded
等指標進行優化的主要緣由之一是,它們做爲瀏覽器中的事件,易於在真實用戶上進行測量。
相比之下,不少其餘指標從來很難測量。例如,咱們常常看到開發人員用這段折中的代碼來測量長任務:
(function detectLongFrame() {
var lastFrameTime = Date.now();
requestAnimationFrame(function() {
var currentFrameTime = Date.now();
if (currentFrameTime - lastFrameTime > 50) {
// Report long frame here...
}
detectLongFrame(currentFrameTime);
});
}());
複製代碼
此代碼使用requestAnimationFrame
循環記錄每次迭代的時間。若是當前時間比前一次超過50毫秒,則認爲這是長任務。 雖然這些代碼起做用,但它有不少缺點:
Lighthouse
和 Web Page Test
雖然提供這些新的性能指標已經有一段時間了(他們是項目發佈前進行性能測試的絕佳工具),可是畢竟他們不是運行在用戶設備上,仍是沒辦法衡量web項目在用戶設備上的實際性能表現。
幸運的是,瀏覽器提供了一些新API,這些新API使得統計真實用戶設備的性能指標變得很簡單,不須要再使用一些影響頁面性能的變通方法。
這些新的API是PerformanceObserver
,PerformanceEntry
和DOMHighResTimeStamp
。接下來咱們經過一個例子,來了解一下怎麼經過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
接口,它的好處是以異步的方式獲取數據,而不是經過不斷的輪詢。
獲取到某個性能數據後,能夠將該用戶的設備的性能數據發送到任意的數據分析服務。好比咱們將首次繪製的指標發送到谷歌統計。
<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。
Steve Souders有一篇名爲User Timing And Custom Metrics的精彩文章,詳細介紹了許多使用瀏覽器性能API肯定什麼時候能夠看到各類類型媒體的技術。
從長遠來看,咱們但願經過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代碼會影響用戶體驗的第一步。
一旦開始收集真實用戶的性能指標,你須要將這些數據付諸實踐。真實用戶性能數據之因此重要主要是因爲如下幾個緣由:
雖然這裏的數據是特定於應用的(你應該本身測試一下本身應用的數據),下面的例子是一個基於性能指標生成的分析報告:
桌面端
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
。
你能夠經過從文檔的<head>
中刪除任何阻塞渲染的腳本或樣式表來縮短首次繪製和首次內容繪製的時間。
花時間肯定用戶感知"it's happening"所需的最小樣式集,並將其內聯到<head>
中,(或者經過HTTP2服務推送),你將得到難以置信的快速首次繪製時間。
PWA中應用的app shell 模式就是一個應用典範。
一旦肯定了頁面上最關鍵的UI元素,你應該確保加載的初始腳本僅包含使這些元素正常渲染和交互的代碼。
任何與關鍵元素無關的代碼包含在初始js模塊中都會拖慢可交互時間。咱們沒有理由強制用戶下載和解析暫時不須要的js代碼。
通用的作法是,你應該儘量的壓縮FMP和TTI之間的時間間隔。若是不能壓縮的話,清楚地提示用戶當前用戶還不能交互是很必要的。
最讓用戶煩躁的體驗就是點擊一個元素,然而什麼也沒發生。
js代碼分割,優化js的加載順序,不只可讓頁面可交互時間變快,還能減小長任務,減小因爲長任務致使的輸入延遲和慢幀。
除了將代碼拆分爲單獨的文件以外,還能夠將同步的大代碼塊拆分爲異步執行的小代碼塊,或者推遲到下一個空閒點。經過以較小代碼塊的方式異步執行該邏輯,你能夠在主線程上留出空間,讓瀏覽器響應用戶輸入。
最後,應該確保引用的第三方代碼進行了長任務相關的測試。致使大量長任務的第三方廣告或者統計腳本最終會損害你的業務。
本文主要關注真實用戶的性能測量,雖然真實用戶數據是最終關注的性能數據,但測試環境數據對於確保您的應用在發佈新功能以前表現良好(而且不會退化)相當重要。測試階段對於退化檢測很是理想,由於它們在受控環境下運行,而且不易受真實用戶環境的隨機變異性影響。
像 Lighthouse
和 Web Page Test
這樣的工具能夠集成到持續集成服務器中,而且若是關鍵指標退化或降低到特定閾值如下,可讓構建失敗。
對於已經發布的代碼,能夠添加自定義警報,當性能指標變差時及時通知你。例如,若是第三方發佈了新代碼,而且你的用戶忽然出現了不少的長任務,會警報通知你。
要成功防止性能退化,你須要在每一個新功能版本中,都進行測試和真實用戶環境下的性能測試。
去年,咱們在瀏覽器上向開發人員開放以用戶爲中心的指標方面取得了重大進展,但尚未完成,而且還有更多已規劃的事情要作。
咱們很是但願將可交互時間和關鍵元素顯示時間統計標準化,所以開發人員無需本身計算這些內容,也不須要依賴polyfills去實現。咱們還但願讓開發人員更容易定位致使丟幀和輸入延遲的長任務和具體的代碼位置。
雖然咱們有更多的工做要作,但咱們對取得的進展感到興奮。有了像PerformanceObserver
這樣的新API以及瀏覽器自己支持的長任務,開發人員可使用js原生的API來測量真實用戶的性能而不會下降用戶體驗。
最重要的指標是那些表明真實用戶體驗的指標,咱們但願開發人員儘量輕鬆地使用戶滿意並建立出色的應用程序。