長動畫幀計時
長動畫幀(Long Animation Frames,LoAFs)會影響網站的使用者體驗。它們可能導致使用者介面(UI)更新緩慢,從而使得控制元件看似無響應,動畫效果和滾動出現卡頓(或不流暢),進而引起使用者沮喪。長動畫幀 API 允許開發者獲取關於長動畫幀的資訊,從而更好地理解其根本原因。本文將展示如何使用長動畫幀 API。
什麼是長動畫幀?
長動畫幀 (LoAF) 是指渲染更新延遲超過 50 毫秒的情況。
良好的響應能力意味著頁面能夠快速響應互動。這包括及時繪製使用者所需的任何更新,並避免任何可能阻礙這些更新的情況。例如,Google 的互動到下一幀渲染 (INP) 指標建議網站應在 200 毫秒內響應頁面互動(如點選或按鍵)。
為了使動畫流暢,更新必須快速——為了使動畫以每秒 60 幀流暢執行,每個動畫幀應在大約 16 毫秒內渲染(1000/60)。
觀察長動畫幀
要獲取 LoAF 的資訊並找出問題所在,您可以使用標準的 PerformanceObserver 觀察 entryType 為 "long-animation-frame" 的效能時間線條目。
const observer = new PerformanceObserver((list) => {
console.log(list.getEntries());
});
observer.observe({ type: "long-animation-frame", buffered: true });
也可以使用 Performance.getEntriesByType() 等方法查詢先前的長動畫幀。
const loafs = performance.getEntriesByType("long-animation-frame");
但是請注意,"long-animation-frame" 條目型別的最大緩衝區大小為 200,超過此限制後新條目將被丟棄,因此建議使用 PerformanceObserver 方法。
檢查 "long-animation-frame" 條目
型別為 "long-animation-frame" 的效能時間線條目由 PerformanceLongAnimationFrameTiming 物件表示。此物件具有一個 scripts 屬性,其中包含一個 PerformanceScriptTiming 物件的陣列,每個物件都包含有關導致長動畫幀的指令碼的資訊。
以下是一個完整的 "long-animation-frame" 效能條目示例,其中包含單個指令碼。
({
blockingDuration: 0,
duration: 60,
entryType: "long-animation-frame",
firstUIEventTimestamp: 11801.099999999627,
name: "long-animation-frame",
renderStart: 11858.800000000745,
scripts: [
{
duration: 45,
entryType: "script",
executionStart: 11803.199999999255,
forcedStyleAndLayoutDuration: 0,
invoker: "DOMWindow.onclick",
invokerType: "event-listener",
name: "script",
pauseDuration: 0,
sourceURL: "https://web.dev/js/index-ffde4443.js",
sourceFunctionName: "myClickHandler",
sourceCharPosition: 17796,
startTime: 11803.199999999255,
window: {
// …Window object…
},
windowAttribution: "self",
},
],
startTime: 11802.400000000373,
styleAndLayoutStart: 11858.800000000745,
});
除了 PerformanceEntry 條目返回的標準資料外,它還包含以下值得注意的項:
blockingDuration-
一個
DOMHighResTimeStamp,表示主執行緒被阻塞,無法響應高優先順序任務(例如使用者輸入)的總毫秒時間。它的計算方式是:獲取 LoAF 中所有持續時間超過50ms的長任務,從每個任務中減去50ms,將渲染時間加到最長任務時間上,然後將結果求和。 firstUIEventTimestamp-
一個
DOMHighResTimeStamp,表示在當前動畫幀期間處理的第一個 UI 事件(例如滑鼠或鍵盤事件)的時間。請注意,如果事件發生和處理之間存在延遲,此時間戳可能在動畫幀開始之前。 renderStart-
一個
DOMHighResTimeStamp,表示渲染週期的開始時間,包括Window.requestAnimationFrame()回撥、樣式和佈局計算、ResizeObserver回撥以及IntersectionObserver回撥。 styleAndLayoutStart-
一個
DOMHighResTimeStamp,表示當前動畫幀的樣式和佈局計算開始的時間段。 PerformanceScriptTiming屬性-
提供有關導致 LoAF 的指令碼資訊的屬性。
script.executionStart-
一個
DOMHighResTimeStamp,表示指令碼編譯完成並開始執行的時間。 script.forcedStyleAndLayoutDuration-
一個
DOMHighResTimeStamp,表示指令碼處理強制佈局/樣式所花費的總毫秒時間。請參閱避免佈局抖動以瞭解其原因。 script.invoker和script.invokerType-
字串值,表示指令碼是如何呼叫的(例如,
"IMG#id.onload"或"Window.requestAnimationFrame"),以及指令碼入口點型別(例如,"event-listener"或"resolve-promise")。 script.pauseDuration-
一個
DOMHighResTimeStamp,表示指令碼在“暫停”同步操作(例如,Window.alert()呼叫或同步XMLHttpRequest)上花費的總毫秒時間。 script.sourceCharPosition、script.sourceFunctionName和script.sourceURL-
分別表示指令碼字元位置、函式名和指令碼 URL 的值。需要注意的是,報告的函式名將是指令碼的“入口點”(即堆疊的頂層),而不是任何特定的慢速子函式。
例如,如果事件處理程式呼叫一個頂層函式,而該頂層函式又呼叫一個慢速子函式,則
source*欄位將報告頂層函式的名稱和位置,而不是慢速子函式。這是出於效能原因——完整的堆疊跟蹤開銷很大。 script.windowAttribution和script.window-
一個列舉值,描述了該指令碼執行所在的容器(即頂層文件或
)與頂層文件之間的關係,以及對其Window物件的引用。
注意:指令碼歸因僅提供給在頁面主執行緒中執行的指令碼,包括同源的
。然而,跨域的、Web Workers、Service Workers 和擴充套件程式碼在長動畫幀中不會有指令碼歸因,即使它們影響了長動畫幀的持續時間。
計算時間戳
PerformanceLongAnimationFrameTiming 類中提供的時間戳允許計算長動畫幀的幾個進一步有用的時間:
| 時序 | 計算 |
|---|---|
| 開始時間 | startTime |
| 結束時間 | startTime + duration |
| 工作持續時間 | renderStart ? renderStart - startTime : duration |
| 渲染持續時間 | renderStart ? (startTime + duration) - renderStart : 0 |
| 渲染:預佈局持續時間 | styleAndLayoutStart ? styleAndLayoutStart - renderStart : 0 |
| 渲染:樣式和佈局持續時間 | styleAndLayoutStart ? (startTime + duration) - styleAndLayoutStart : 0 |
示例
長動畫幀 API 功能檢測
您可以使用 PerformanceObserver.supportedEntryTypes 測試是否支援長動畫幀 API。
if (PerformanceObserver.supportedEntryTypes.includes("long-animation-frame")) {
// Monitor LoAFs
}
報告超過特定閾值的 LoAF
雖然 LoAF 的閾值固定為 50 毫秒,但在您首次開始效能最佳化工作時,這可能會導致大量報告。最初,您可能希望報告較高閾值值的 LoAF,並隨著您改進網站和消除最嚴重的 LoAF 而逐漸降低閾值。以下程式碼可用於捕獲超過特定閾值的 LoAF 以進行進一步分析(例如,透過將它們傳送回分析端點)。
const REPORTING_THRESHOLD_MS = 150;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > REPORTING_THRESHOLD_MS) {
// Example here logs to console; real code could send to analytics endpoint
console.log(entry);
}
}
});
observer.observe({ type: "long-animation-frame", buffered: true });
長動畫幀條目可能相當大;因此,請仔細考慮每個條目中應傳送到分析的資料。例如,條目的摘要時間以及指令碼 URL 可能足以滿足您的需求。
觀察最長的動畫幀
您可能只希望收集最長動畫幀的資料(例如前 5 或 10 個),以減少需要收集的資料量。這可以按如下方式處理:
MAX_LOAFS_TO_CONSIDER = 10;
let longestBlockingLoAFs = [];
const observer = new PerformanceObserver((list) => {
longestBlockingLoAFs = longestBlockingLoAFs
.concat(list.getEntries())
.sort((a, b) => b.blockingDuration - a.blockingDuration)
.slice(0, MAX_LOAFS_TO_CONSIDER);
});
observer.observe({ type: "long-animation-frame", buffered: true });
// Report data on visibilitychange event
document.addEventListener("visibilitychange", () => {
// Example here logs to console; real code could send to analytics endpoint
console.log(longestBlockingLoAFs);
});
報告帶有互動的長動畫幀
另一種有用的技術是傳送最大的 LoAF 條目,其中幀期間發生了互動,這可以透過 firstUIEventTimestamp 值的存在來檢測。
以下程式碼記錄了所有大於 150 毫秒且在幀期間發生互動的 LoAF 條目。您可以根據需要選擇更高或更低的值。
const REPORTING_THRESHOLD_MS = 150;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (
entry.duration > REPORTING_THRESHOLD_MS &&
entry.firstUIEventTimestamp > 0
) {
// Example here logs to console; real code could send to analytics endpoint
console.log(entry);
}
}
});
observer.observe({ type: "long-animation-frame", buffered: true });
識別長動畫幀中的常見指令碼模式
另一種策略是檢視哪些指令碼在 LoAF 條目中最常出現。可以報告指令碼和/或字元位置級別的資料,以識別最有問題的指令碼。這在跨多個站點使用導致效能問題的主題或外掛的情況下很有用。
LoAF 中常見指令碼(或第三方來源)的執行時間可以求和並報告,以識別整個站點或站點集合中導致 LoAF 的常見貢獻者。
例如,按 URL 分組指令碼並顯示總持續時間:
const observer = new PerformanceObserver((list) => {
const allScripts = list.getEntries().flatMap((entry) => entry.scripts);
const scriptSource = [
...new Set(allScripts.map((script) => script.sourceURL)),
];
const scriptsBySource = scriptSource.map((sourceURL) => [
sourceURL,
allScripts.filter((script) => script.sourceURL === sourceURL),
]);
const processedScripts = scriptsBySource.map(([sourceURL, scripts]) => ({
sourceURL,
count: scripts.length,
totalDuration: scripts.reduce(
(subtotal, script) => subtotal + script.duration,
0,
),
}));
processedScripts.sort((a, b) => b.totalDuration - a.totalDuration);
// Example here logs to console; real code could send to analytics endpoint
console.table(processedScripts);
});
observer.observe({ type: "long-animation-frame", buffered: true });
與長任務 API 比較
長動畫幀 API 的前身是長任務 API(參見PerformanceLongTaskTiming)。這兩個 API 的目的和用法相似——揭示關於阻塞主執行緒 50 毫秒或更長時間的長任務的資訊。
減少網站上發生的長任務數量很有用,因為長任務可能導致響應性問題。例如,如果使用者在主執行緒正在處理長任務時單擊按鈕,則對單擊的 UI 響應將被延遲,直到長任務完成。傳統觀點是將長任務分解為多個較小的任務,以便在任務之間處理重要的互動。
然而,長任務 API 也有其侷限性:
- 一個動畫幀可能由幾個低於 50ms 閾值的任務組成,但它們仍然集體阻塞主執行緒。長動畫幀 API 透過將動畫幀視為一個整體來解決這個問題。
PerformanceLongTaskTiming條目型別比PerformanceLongAnimationFrameTiming型別暴露的資訊更有限——例如,它可以告訴您長任務發生的容器,但不能告訴您導致它的指令碼或函式。- 長任務 API 提供了一個不完整的檢視,因為它可能排除一些重要的任務。一些更新(例如渲染)發生在單獨的任務中,理想情況下應該與導致該更新的先行執行一起包含,以準確測量該互動的“總工作量”。