1. 簡介
這是互動式程式碼研究室,可讓您瞭解如何使用 web-vitals
程式庫評估與下一個顯示的內容互動 (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」。您隨時可以在開發人員工具頂端的分頁標籤之間切換。
- INP 問題最常發生在行動裝置上,因此請改用行動裝置多媒體模擬。
- 如果您使用桌上型電腦或筆記型電腦進行測試,效能可能會遠高於實體行動裝置。如要查看更實際的效能檢查結果,請點選「Performance」面板右上角的齒輪,然後選取「CPU 4 倍減速」。
4. 安裝中 web-vitals
web-vitals
是 JavaScript 程式庫,可用於評估使用者體驗的 Web Vitals 指標。您可以利用這個程式庫擷取這些值,並引導他們至數據分析端點,以便之後進行分析,進而找出互動速度緩慢的時間和位置。
將程式庫新增至頁面的方法有很多種,您將如何在自己的網站上安裝程式庫,取決於您管理依附元件的方式、建構程序和其他因素。請務必查看程式庫的說明文件,瞭解所有可用選項。
本程式碼研究室會從 npm 安裝並直接載入指令碼,避免深入瞭解特定建構程序。
您可以使用兩種版本的 web-vitals
:
- 「標準」如要追蹤網頁載入中 Core Web Vitals 的指標值,請使用 build。
- 「歸因」版本會在每個指標中加入額外的偵錯資訊,以便診斷指標得出其本身值的原因。
為了在本程式碼研究室中評估 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 時同步觸發,因此會造成點擊的互動速度變慢。就是這樣
按一下「Yes」接受 (試用版) Cookie,然後查看開發人員工具控制台中記錄的 INP 資料。
網頁 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: [{...}]
}],
這裡有許多實用的值,例如細分樣式所花費的時間。如要進一步瞭解這些屬性,請參閱長動畫影格 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
}]
在這個範例中,我們可以分辨時間主要是在單一 event-listener
中,於 BUTTON#confirm.onclick
叫用。我們甚至可以看到指令碼來源網址和定義函式的字元位置!
重點摘要
這個歸因資料如何判斷這種狀況?
- 互動是由按下
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
元素互動的 INP 值 (已啟用 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
。這是在這個指令碼叫用中,被強制瀏覽器重新安排網頁配置的時間。換句話說,在 497 毫秒裡,有 78% 的時間 (用來執行這個事件監聽器的時間為 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);