聚焦于 Web 性能指標(biāo) TTI
在優(yōu)化網(wǎng)站性能的過程中,我們經(jīng)常遇到一個(gè)“為指標(biāo)而優(yōu)化”的困境。指標(biāo)并不能真正反映用戶體驗(yàn),而應(yīng)該最真實(shí)地反映用戶行為。
在本節(jié)中,我們將研究 TTI(Time to Interactive)。在深入探討這個(gè)話題之前,我們先了解一些背景知識(shí)。
RAIL 模型
RAIL 是一個(gè)以用戶為中心的性能模型。每個(gè) web 應(yīng)用程序在其生命周期中都有四個(gè)不同的方面,這些方面以不同的方式影響性能:
圖片
1.響應(yīng):輸入延遲時(shí)間(從按下到繪制)小于 100 毫秒。
- 用戶按下一個(gè)按鈕(例如打開導(dǎo)航)。
2.動(dòng)畫:每幀工作的完成時(shí)間(從 JS 到繪制)小于 16 毫秒。
- 用戶滾動(dòng)頁面,拖動(dòng)手指(例如打開菜單)或看到動(dòng)畫。當(dāng)拖動(dòng)時(shí),應(yīng)用程序的響應(yīng)應(yīng)該與手指位置相關(guān)(例如下拉刷新,滑動(dòng)輪播)。此指標(biāo)僅適用于拖動(dòng)的連續(xù)階段,而不適用于初始階段。
3.空閑:主線程 JS 工作被分成不超過 50 毫秒的塊。
- 用戶不與頁面交互,但主線程應(yīng)有足夠的時(shí)間處理下一個(gè)用戶輸入。
4.加載:頁面可以在 1000 毫秒內(nèi)準(zhǔn)備就緒。
- 用戶加載頁面并看到關(guān)鍵路徑內(nèi)容。
如果你想提高網(wǎng)站的用戶體驗(yàn),RAIL 是一個(gè)很好的評(píng)估模型。
解釋 TTI(Time to Interactive)
TTI 是指應(yīng)用程序已經(jīng)可視化渲染并且可以響應(yīng)用戶輸入的時(shí)間。為了理解 “TTI”,我們需要了解它的計(jì)算規(guī)則。我們看看下圖:
圖片
在官方文檔中找到了以下描述:
First Idle 是主線程第一次靜止并且瀏覽器完成第一次有意義繪制的早期標(biāo)志。
Time to Interactive 是在第一次有意義繪制之后。瀏覽器的主線程已經(jīng)靜止至少 5 秒,并且沒有長任務(wù)會(huì)阻止立即響應(yīng)用戶輸入。
我們可以簡單地理解為:
First Idle 是主線程處于靜止?fàn)顟B(tài)并且瀏覽器完成了第一次有意義繪制的早期標(biāo)志;TTI 發(fā)生在 FMP 之后,瀏覽器的主線程保持空閑至少 5 秒,沒有任何可能阻止用戶交互響應(yīng)的“長任務(wù)”。
長任務(wù)
對于“長任務(wù)”,我們?nèi)鐖D所示:
圖片
對于用戶來說,長任務(wù)時(shí)間表現(xiàn)為卡頓或滯后,這也是當(dāng)前糟糕的網(wǎng)絡(luò)體驗(yàn)的主要根源。
如何測量長任務(wù)?
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// TODO...
console.log(entry);
}
});
observer.observe({entryTypes: ['longtask']});
控制臺(tái)輸出如下:
{
"name": "self",
"entryType": "longtask",
"startTime": 315009.59500001045,
"duration": 99.9899999878835,
"attribution": [
{
"name": "unknown",
"entryType": "taskattribution",
"startTime": 0,
"duration": 0,
"containerType": "window",
"containerSrc": "",
"containerId": "",
"containerName": ""
}
]
}
長任務(wù) API 可以將任何超過 50 毫秒的任務(wù)標(biāo)記為潛在問題,并向應(yīng)用程序開發(fā)人員展示這些任務(wù)。選擇 50 毫秒是為了確保應(yīng)用程序滿足 RAIL 性能準(zhǔn)則,即在 100 毫秒內(nèi)響應(yīng)用戶輸入。
在實(shí)際開發(fā)中,我們可以使用一種 hack 方法來檢查頁面代碼中的“長任務(wù)”:
// 檢測長任務(wù) hack
(function detectLongFrame() {
let lastFrameTime = Date.now();
requestAnimationFrame(function() {
let currentFrameTime = Date.now();
if (currentFrameTime - lastFrameTime > 50) {
// 在這里報(bào)告長幀...
}
detectLongFrame(currentFrameTime);
});
}());
如何計(jì)算 TTI?
在計(jì)算之前,我們先看看 Timing API:
圖片
在官方的 Google 文檔中,有如下描述:
注意:DOM 交互完成后的最小 FMP 值 DOM 交互完成是所有 DOMContentLoaded 監(jiān)聽器執(zhí)行完畢的時(shí)間點(diǎn)。通常,頁面的關(guān)鍵事件監(jiān)聽器很少在此時(shí)間點(diǎn)之前安裝。我們實(shí)驗(yàn)的一些 firstInteractive 定義只查看長任務(wù)和網(wǎng)絡(luò)活動(dòng)(而不是查看安裝了多少事件監(jiān)聽器),有時(shí)在加載的前 5-10 秒內(nèi)沒有長任務(wù),我們會(huì)在 FMP 時(shí)觸發(fā) FirstInteractive,而此時(shí)網(wǎng)站通常還沒有準(zhǔn)備好處理用戶輸入。我們發(fā)現(xiàn),如果我們將 max(DOMContentLoadedEnd, firstInteractive) 作為最終的 firstInteractive 值,返回的值在合理范圍內(nèi)。等待 DOMContentLoadedEnd 來聲明 FirstInteractive 是合理的,因此所有下面介紹的定義都在 DOMContentLoadedEnd 時(shí)降低了 firstInteractive 的下限。
因此,我們可以大致估算使用 domContentLoadedEventEnd:
TTI: domContentLoadedEventEnd - navigationStart,
domContentLoadedEventEnd: 文檔 DOMContentLoaded 事件結(jié)束的時(shí)間。
domContentLoadedEventEnd 屬性必須返回一個(gè)具有時(shí)間值的 DOMHighResTimeStamp,該值等于當(dāng)前文檔的 DOMContentLoaded 事件完成后的時(shí)間。
如果你覺得上述計(jì)算過于復(fù)雜,可以使用 Google 提供的 Polyfill 來獲取。
TTI 指標(biāo)監(jiān)控
我們可以使用 Google TTI Polyfill 監(jiān)控 TTI。
npm install tti-polyfill
使用
import ttiPolyfill from './path/to/tti-polyfill.js';
ttiPolyfill.getFirstConsistentlyInteractive(opts).then((tti) => {
// 使用 `tti` 值進(jìn)行一些操作。
});
結(jié)論
通過研究 TTI,我們可以更好地理解如何提高網(wǎng)頁的交互性能。RAIL 模型為評(píng)估用戶體驗(yàn)提供了一個(gè)框架,而 TTI 則是衡量網(wǎng)頁何時(shí)可以交互的重要指標(biāo)。通過檢測和優(yōu)化長任務(wù),我們可以顯著改善用戶體驗(yàn),并確保網(wǎng)頁在加載后盡快變得可交互。