1. 簡介
本程式碼研究室會以互動方式,說明如何使用 web-vitals
程式庫評估 Interaction to Next Paint (INP)。
必要條件
- 具備 HTML 和 JavaScript 開發知識。
- 建議:請參閱 web.dev INP 指標說明文件。
學習目標
- 如何在網頁中加入
web-vitals
程式庫,並使用其歸因資料。 - 運用歸因資料診斷 INP 的改善方向和方法。
軟硬體需求
- 電腦:可從 GitHub 複製程式碼並執行 npm 指令。
- 文字編輯器。
- 最新版 Chrome,才能使用所有互動評估功能。
2. 做好準備
取得並執行程式碼
您可以在 web-vitals-codelabs
存放區中找到這段程式碼。
- 在終端機中複製存放區:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
。 - 進入複製的目錄:
cd web-vitals-codelabs/measuring-inp
。 - 安裝依附元件:
npm ci
。 - 啟動網路伺服器:
npm run start
。 - 使用瀏覽器前往 http://localhost:8080/。
試用頁面
本程式碼研究室會使用 Gastropodicon (熱門的蝸牛解剖參考網站),探討 INP 的潛在問題。
嘗試與網頁互動,瞭解哪些互動速度較慢。
3. 熟悉 Chrome 開發人員工具
開啟開發人員工具:從「更多工具」 >「開發人員工具」選單在網頁上按一下滑鼠右鍵,然後選取「檢查」,或使用鍵盤快速鍵。
在本程式碼研究室中,我們會同時使用「Performance」面板和「Console」。您隨時可以透過 DevTools 頂端的分頁切換這些選項。
- INP 問題最常發生在行動裝置上,因此請切換為行動裝置顯示模擬功能。
- 如果您在桌上型電腦或筆記型電腦上進行測試,效能可能會比在實際行動裝置上測試時好上許多。如要更準確地查看效能,請按一下「Performance」面板右上方的齒輪圖示,然後選取「CPU 減速 4 倍」。
4. 安裝 web-vitals
web-vitals
是 JavaScript 程式庫,可評估使用者體驗的網站使用體驗核心指標。您可以使用程式庫擷取這些值,然後將這些值發送至數據分析端點,以利日後分析,以便瞭解發生緩慢互動情形的時間和地點。
將圖書館新增至網頁的方法有幾種。您在自有網站上安裝程式庫的方式,取決於您管理依附元件的方式、建構程序和其他因素。請務必查看程式庫的說明文件,瞭解所有選項。
本程式碼研究室會從 npm 安裝並直接載入指令碼,避免深入特定建構程序。
您可以使用兩個版本的 web-vitals
:
- 如要追蹤網頁載入時的 Core Web Vitals 指標值,請使用「標準」版本。
- 「歸因」版本會為每個指標新增額外的偵錯資訊,以便診斷指標為何會產生該值。
為了在本程式碼研究室中評估 INP,我們需要屬性建構。
執行 npm install -D web-vitals
,將 web-vitals
新增至專案的 devDependencies
將 web-vitals
新增至頁面:
將歸因版本的指令碼新增至 index.html
底部,並將結果記錄到控制台:
<script type="module">
import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';
onINP(console.log);
</script>
立即試用
開啟控制台後,請再次嘗試與網頁互動。您在頁面上點選各處,但系統不會記錄任何內容!
INP 會在網頁的整個生命週期中進行評估,因此根據預設,web-vitals
會在使用者離開或關閉網頁後才回報 INP。這對於用於數據分析等用途的訊號發送行為來說是理想的做法,但不太適合用於互動式偵錯。
web-vitals
提供reportAllChanges
選項,可產生更詳細的報表。啟用後,系統不會回報「每個」互動,但會回報每個互動速度比前一個慢的互動。
請嘗試在指令碼中加入選項,然後再次與頁面互動:
<script type="module">
import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';
onINP(console.log, {reportAllChanges: true});
</script>
重新整理頁面後,互動資料就會回報至控制台,並在有新的最慢互動時更新。舉例來說,請嘗試在搜尋框中輸入內容,然後刪除輸入內容。
5. 歸因資訊包含哪些內容?
我們先從大多數使用者與網頁互動的最初階段開始,也就是 Cookie 同意聲明對話方塊。
許多網頁都會在使用者接受 Cookie 時,同步觸發 Cookie 的程式碼,導致點擊互動速度變慢。這就是發生的情況。
按一下「是」接受 (示範) Cookie,然後查看現在記錄至 DevTools 主控台的 INP 資料。
這項頂層資訊可在標準和歸因 Web Vitals 版本中使用:
{
name: 'INP',
value: 344,
rating: 'needs-improvement',
entries: [...],
id: 'v4-1715732159298-8028729544485',
navigationType: 'reload',
attribution: {...},
}
從使用者點選到下一個繪製作業的時間長度為 344 毫秒,屬於「需要改善」的 INP。entries
陣列包含與此互動相關聯的所有 PerformanceEntry
值,在本例中只有一個點擊事件。
不過,我們最想瞭解的是 attribution
屬性,以便找出這個時間點發生的情況。web-vitals
會找出哪個長動畫影格 (LoAF) 與點擊事件重疊,以便建立歸因資料。接著,LoAF 就能提供詳細資料,說明該影格中花費的時間,從執行的指令碼,到 requestAnimationFrame
回呼、樣式和版面配置所花費的時間。
展開 attribution
屬性即可查看更多資訊。資料會更加豐富。
attribution: {
interactionTargetElement: Element,
interactionTarget: '#confirm',
interactionType: 'pointer',
inputDelay: 27,
processingDuration: 295.6,
presentationDelay: 21.4,
processedEventEntries: [...],
longAnimationFrameEntries: [...],
}
首先,您會看到與哪些項目互動的資訊:
interactionTargetElement
:對已互動元素的即時參照 (如果元素未從 DOM 中移除)。interactionTarget
:用於在頁面中尋找元素的選取器。
接著,我們將時間細分為以下幾個階段:
inputDelay
:從使用者開始互動 (例如按下滑鼠) 到該互動事件的事件監聽器開始執行之間的時間。在本例中,即使 CPU 節流功能已開啟,輸入延遲時間也只有約 27 毫秒。processingDuration
:事件監聽器執行完畢所需的時間。通常,網頁會為單一事件 (例如pointerdown
、pointerup
和click
) 提供多個監聽器。如果這些監聽器都在同一個動畫影格中執行,就會合併為這段時間。在本例中,處理時間為 295.6 毫秒,佔 INP 時間的大部分。presentationDelay
:從事件監聽器完成後,到瀏覽器完成繪製下一個影格所需的時間。在本例中為 21.4 毫秒。
這些 INP 階段可提供重要信號,協助您診斷需要進行哪些最佳化。如要進一步瞭解這項主題,請參閱這份 INP 最佳化指南。
進一步深入瞭解,processedEventEntries
包含五個事件,而非頂層 INP entries
陣列中的單一事件。其中有何區別?
processedEventEntries: [
{
name: 'mouseover',
entryType: 'event',
startTime: 1801.6,
duration: 344,
processingStart: 1825.3,
processingEnd: 1825.3,
cancelable: true
},
{
name: 'mousedown',
entryType: 'event',
startTime: 1801.6,
duration: 344,
processingStart: 1825.3,
processingEnd: 1825.3,
cancelable: true
},
{name: 'mousedown', ...},
{name: 'mouseup', ...},
{name: 'click', ...},
],
頂層項目是 INP 事件,在本例中為點擊事件。歸因 processedEventEntries
是指在同一個影格中處理的所有事件。請注意,這項事件包含 mouseover
和 mousedown
等其他事件,而非只有點擊事件。如果這些事件的速度也偏慢,瞭解這些事件可能非常重要,因為這些事件都會導致回應速度變慢。
最後是 longAnimationFrameEntries
陣列。這可能是一個單一項目,但在某些情況下,互動可能會分散在多個影格中。這裡是單一長動畫影格最簡單的情況。
longAnimationFrameEntries
展開 LoAF 項目:
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 1823,
duration: 319,
renderStart: 2139.5,
styleAndLayoutStart: 2139.7,
firstUIEventTimestamp: 1801.6,
blockingDuration: 268,
scripts: [{...}]
}],
這裡有許多實用的值,例如分解花費在樣式上的時間。如要進一步瞭解這些屬性,請參閱「Long Animation Frames API」一文。我們目前主要關注 scripts
屬性,其中包含詳細資料的項目,可提供負責長時間執行影格的指令碼:
scripts: [{
name: 'script',
invoker: 'BUTTON#confirm.onclick',
invokerType: 'event-listener',
startTime: 1828.6,
executionStart: 1828.6,
duration: 294,
sourceURL: 'http://localhost:8080/third-party/cmp.js',
sourceFunctionName: '',
sourceCharPosition: 1144
}]
在本例中,我們可以得知時間主要花費在 BUTTON#confirm.onclick
上叫用的單一 event-listener
上。我們甚至可以看到指令碼來源網址,以及函式定義的字元位置!
外帶
我們可以從歸因資料判斷這個案例的哪些情況?
- 互動是因點選
button#confirm
元素 (來自attribution.interactionTarget
和指令碼歸因項目的invoker
屬性) 而觸發。 - 時間主要用於執行事件監聽器 (從
attribution.processingDuration
與總指標value
比較而得)。 - 慢速事件監聽器程式碼會從
third-party/cmp.js
中定義的點擊事件監聽器 (從scripts.sourceURL
) 開始。
這些資料足以讓我們瞭解需要改善的部分!
6. 多個事件監聽器
重新整理頁面,清除開發人員工具控制台,並讓 Cookie 同意聲明互動不再是持續時間最長的互動。
在搜尋框中輸入內容。歸因資料會顯示哪些資訊?你認為發生了什麼事?
歸因分析資料
首先,我們將概略掃描一個測試範例:
{
name: 'INP',
value: 1072,
rating: 'poor',
attribution: {
interactionTargetElement: Element,
interactionTarget: '#search-terms',
interactionType: 'keyboard',
inputDelay: 3.3,
processingDuration: 1060.6,
presentationDelay: 8.1,
processedEventEntries: [...],
longAnimationFrameEntries: [...],
}
}
這是使用鍵盤與 input#search-terms
元素互動時,由於 CPU 節流而導致的 INP 值不佳。處理時間長度為 1072 毫秒,其中 1061 毫秒花在處理時間。
不過,scripts
項目更有趣。
版面配置耗盡資源
scripts
陣列的第一個項目會提供一些寶貴的背景資訊:
scripts: [{
name: 'script',
invoker: 'BUTTON#confirm.onclick',
invokerType: 'event-listener',
startTime: 4875.6,
executionStart: 4875.6,
duration: 497,
forcedStyleAndLayoutDuration: 388,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: 'handleSearch',
sourceCharPosition: 940
},
...]
大部分的處理時間都發生在這個指令碼執行期間,也就是 input
事件監聽器 (叫用者為 INPUT#search-terms.oninput
)。函式名稱 (handleSearch
) 和 index.js
來源檔案中的字元位置都會提供。
不過,我們也新增了 forcedStyleAndLayoutDuration
屬性。這是在這個指令碼叫用中花費的時間,瀏覽器會在此期間被迫重新排版網頁。換句話說,執行這個事件監聽器所花費的時間中,有 78% (497 毫秒中的 388 毫秒) 實際上是花在版面配置耗損上。
這應該是優先修正的問題。
重複監聽器
就個別而言,下列兩個指令碼項目並無特別之處:
scripts: [...,
{
name: 'script',
invoker: '#document.onkeyup',
invokerType: 'event-listener',
startTime: 5375.3,
executionStart: 5375.3,
duration: 124,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: '',
sourceCharPosition: 1526,
},
{
name: 'script',
invoker: '#document.onkeyup',
invokerType: 'event-listener',
startTime: 5673.9,
executionStart: 5673.9,
duration: 95,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: '',
sourceCharPosition: 1526
}]
這兩個項目都是 keyup
事件監聽器,會在另一個事件監聽器之後執行。事件監聽器是匿名函式 (因此 sourceFunctionName
屬性不會回報任何內容),但我們仍有來源檔案和字元位置,因此可以找到程式碼所在位置。
奇怪的是,兩者都來自相同的來源檔案和字元位置。
瀏覽器最終會在單一動畫影格中處理多個按鍵按下動作,導致此事件監聽器在任何內容繪製前執行兩次!
這種效應也會累積,也就是說,事件監聽器完成所需的時間越長,就會有越多額外的輸入事件,導致互動速度變慢的時間更長。
由於這是搜尋/自動完成互動,因此建議您採用去抖動策略,以便每個影格最多只處理一次按鍵按下動作。
7. 輸入延遲時間
輸入延遲的常見原因 (從使用者互動到事件監聽器開始處理互動所需的時間) 是因為主執行緒忙碌。這可能有多種原因:
- 網頁正在載入,而主執行緒正忙於執行 DOM 設定、網頁版面配置和樣式,以及評估及執行指令碼的初始工作。
- 網頁通常很忙碌,例如執行運算、以指令碼為基礎的動畫或廣告。
- 上一個範例中,先前的互動處理時間過長,導致後續互動延遲。
示範頁面有一個秘密功能,如果您按一下頁面頂端的蝸牛標誌,系統就會開始執行動畫,並執行一些繁重的主執行緒 JavaScript 工作。
- 按一下蝸牛標誌即可開始動畫。
- 當蝸牛位於彈跳的底部時,JavaScript 工作會觸發。請盡可能在接近跳出率底部時與網頁互動,看看能觸發多高的 INP。
舉例來說,即使您沒有觸發其他事件監聽器 (例如在蝸牛彈跳時,點選並將焦點放在搜尋方塊上),主執行緒的工作也會導致頁面在一段明顯的時間內無回應。
在許多網頁上,繁重的主執行緒工作不會這麼有禮貌,但這可做為示範,說明如何在 INP 歸因資料中識別這類工作。
以下是僅在蝸牛跳出期間將焦點放在搜尋框的歸因範例:
{
name: 'INP',
value: 728,
rating: 'poor',
attribution: {
interactionTargetElement: Element,
interactionTarget: '#search-terms',
interactionType: 'pointer',
inputDelay: 702.3,
processingDuration: 4.9,
presentationDelay: 20.8,
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 2064.8,
duration: 790,
renderStart: 2065,
styleAndLayoutStart: 2854.2,
firstUIEventTimestamp: 0,
blockingDuration: 740,
scripts: [{...}]
}]
}
}
如預期,事件監聽器執行速度很快,處理時間為 4.9 毫秒,且大部分的互動問題都與輸入延遲有關,總共 728 毫秒中,有 702.3 毫秒是輸入延遲造成的。
這種情況很難偵錯。雖然我們知道使用者與哪些內容互動,以及互動方式,但我們也知道該部分互動很快就完成,因此並無問題。而是頁面上的其他內容延遲了互動開始處理的時間,但我們要如何知道從哪裡開始尋找?
這時,LoAF 指令碼項目就能派上用場:
scripts: [{
name: 'script',
invoker: 'SPAN.onanimationiteration',
invokerType: 'event-listener',
startTime: 2065,
executionStart: 2065,
duration: 788,
sourceURL: 'http://localhost:8080/js/index.js',
sourceFunctionName: 'cryptodaphneCoinHandler',
sourceCharPosition: 1831
}]
雖然此函式與互動無關,但確實會減緩動畫影格,因此會納入與互動事件結合的 LoAF 資料。
從這裡,我們可以瞭解延遲互動處理作業的函式如何觸發 (由 animationiteration
事件監聽器觸發),以及哪個函式負責,以及該函式在來源檔案中的位置。
8. 呈現延遲:更新內容無法繪製時
呈現延遲是指從事件監聽器停止執行到瀏覽器能夠將新影格繪製到畫面,以顯示可見的使用者意見回饋所需的時間。
重新整理頁面,再次重設 INP 值,然後開啟漢堡選單。開啟時會出現明顯的延遲。
這會是什麼樣子?
{
name: 'INP',
value: 376,
rating: 'needs-improvement',
delta: 352,
attribution: {
interactionTarget: '#sidenav-button>svg',
interactionType: 'pointer',
inputDelay: 12.8,
processingDuration: 14.7,
presentationDelay: 348.5,
longAnimationFrameEntries: [{
name: 'long-animation-frame',
startTime: 651,
duration: 365,
renderStart: 673.2,
styleAndLayoutStart: 1004.3,
firstUIEventTimestamp: 138.6,
blockingDuration: 315,
scripts: [{...}]
}]
}
}
這次是呈現延遲造成大部分的互動速度緩慢。也就是說,事件監聽器完成後,任何阻斷主要執行緒的情況都會發生。
scripts: [{
entryType: 'script',
invoker: 'FrameRequestCallback',
invokerType: 'user-callback',
startTime: 673.8,
executionStart: 673.8,
duration: 330,
sourceURL: 'http://localhost:8080/js/side-nav.js',
sourceFunctionName: '',
sourceCharPosition: 1193,
}]
查看 scripts
陣列中的單一項目,我們會看到 FrameRequestCallback
在 user-callback
中花費的時間。這次的呈現延遲是因為 requestAnimationFrame
回呼。
9. 結語
匯總欄位資料
值得一提的是,如果從單一網頁載入中查看單一 INP 歸因項目,這一切就會變得更簡單。如何彙整這類資料,以便根據欄位資料對 INP 進行偵錯?大量實用的詳細資料反而會讓這項工作變得更困難。
舉例來說,瞭解哪些網頁元素是造成互動速度緩慢的常見原因,就非常有幫助。不過,如果網頁的編譯 CSS 類別名稱會因版本而異,則同一個元素的 web-vitals
選取器可能會因版本而異。
您必須考量特定應用程式,判斷最實用的資料,以及如何匯總資料。舉例來說,您可以在發送歸因資料前,根據目標所在的元件或目標執行的 ARIA 角色,將 web-vitals
選取器替換為自己的 ID。
同樣地,scripts
項目的 sourceURL
路徑可能含有檔案式雜湊,因此不易合併,但您可以根據已知的建構程序,在發送資料回報前移除雜湊。
很遺憾,對於這麼複雜的資料,沒有簡單的解決方法,但即使使用其中的子集,也比完全沒有歸因資料的偵錯程序更有價值。
到處都需要標示出處!
以 LoAF 為基礎的 INP 歸因功能是一項強大的偵錯輔助工具。這項工具會提供詳細資料,說明 INP 期間發生的具體情形。在許多情況下,這項工具可指出指令碼中應著手進行最佳化的工作位置。
您現在可以在任何網站上使用 INP 歸因資料!
即使您無法編輯網頁,也可以在開發人員工具控制台中執行以下程式碼片段,瞭解您可以找到哪些資訊,藉此重現本程式碼研究室中的程序:
const script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.iife.js';
script.onload = function () {
webVitals.onINP(console.log, {reportAllChanges: true});
};
document.head.appendChild(script);