瞭解與下一個顯示的內容互動 (INP)

瞭解「與下一個顯示的內容互動」指標 (INP)

程式碼研究室簡介

subject上次更新時間:1月 9, 2025
account_circle作者:Michal Mocny, Brendan Kenny

1. 簡介

本程式碼研究室會以互動示範的方式,介紹 Interaction to Next Paint (INP)

這張圖表描繪主執行緒的互動情形。使用者在工作執行期間輸入內容。輸入內容會延遲到這些工作完成為止,之後指標向上、滑鼠向上和點選事件監聽器會執行,然後啟動算繪和繪製工作,直到顯示下一個影格為止

必要條件

課程內容

  • 使用者互動和您處理這些互動的方式,如何影響網頁回應速度。
  • 如何減少及消除延遲,提供流暢的使用者體驗。

需求條件

  • 電腦必須能夠從 GitHub 複製程式碼,並執行 npm 指令。
  • 文字編輯器。
  • 使用新版 Chrome,才能正常進行所有互動評估。

2. 做好準備

取得並執行程式碼

程式碼位於web-vitals-codelabs 存放區

  1. 在終端機中複製存放區:git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. 前往複製的目錄:cd web-vitals-codelabs/understanding-inp
  3. 安裝依附元件:npm ci
  4. 啟動網路伺服器:npm run start
  5. 在瀏覽器中前往 http://localhost:5173/understanding-inp/

應用程式總覽

頁面頂端會顯示「分數」計數器和「增加」按鈕。這是反應性和回應性的經典示範!

本程式碼研究室的示範應用程式螢幕截圖

按鈕下方有四項測量結果:

  • INP:目前的 INP 分數,通常是最差的互動。
  • 互動:最近一次互動的分數。
  • FPS:網頁的主執行緒每秒影格數。
  • 計時器:顯示執行中的計時器動畫,協助您查看卡頓情形。

測量互動時,完全不需要 FPS 和計時器項目。新增這些項目只是為了方便您查看回應式設計。

立即試用

試著與「Increment」(增加) 按鈕互動,並觀察分數是否增加。INP互動值是否會隨著每次遞增而改變?

INP 會測量從使用者互動到網頁實際向使用者顯示更新內容的時間。

3. 使用 Chrome 開發人員工具評估互動

開啟開發人員工具:依序選取「更多工具」 >「開發人員工具」選單在網頁上按一下滑鼠右鍵,然後選取「檢查」;或使用鍵盤快速鍵

切換至「效能」面板,用來評估互動。

螢幕截圖:開發人員工具的「效能」面板和應用程式並排顯示

接著,在「效能」面板中擷取互動。

  1. 按下 [錄製]。
  2. 與網頁互動 (按下「Increment」按鈕)。
  3. 停止錄製。

時間軸隨即會顯示「互動」軌。按一下左側的三角形即可展開。

動畫:示範如何使用開發人員工具的「效能」面板記錄互動

畫面會顯示兩項互動。捲動或按住 W 鍵,即可放大第二個畫面。

開發人員工具「效能」面板的螢幕截圖,游標懸停在面板中的互動上,工具提示列出互動的短暫時間

將滑鼠游標懸停在互動上,您會看到互動速度很快,完全沒有處理時間,且輸入延遲顯示延遲時間最短,確切時間長度取決於電腦速度。

4. 長時間執行的事件監聽器

開啟 index.js 檔案,並取消事件監聽器中的 blockFor 函式註解。

查看完整程式碼:click_block.html

button.addEventListener('click', () => {
  blockFor
(1000);
  score
.incrementAndUpdateUI();
});

儲存檔案。伺服器會看到這項變更,並為您重新整理頁面。

請再次與頁面互動。現在互動速度會明顯變慢。

效能追蹤

在「效能」面板中再次錄製,看看會顯示什麼內容。

「效能」面板中長達一秒的互動

原本的短暫互動現在需要整整一秒。

將滑鼠懸停在互動上,會發現時間幾乎都花在「處理時間」上,這是執行事件監聽器回呼所花費的時間。由於封鎖 blockFor 呼叫完全位於事件監聽器中,因此時間會花費在該處。

5. 實驗:處理時間

嘗試重新安排事件監聽器工作,看看對 INP 有何影響。

請先更新 UI

如果調換 JS 呼叫的順序,先更新 UI 再封鎖,會發生什麼情況?

查看完整程式碼:ui_first.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
  blockFor
(1000);
});

您是否注意到稍早出現的使用者介面?順序會影響 INP 分數嗎?

請嘗試擷取追蹤記錄並檢查互動,看看是否有任何差異。

個別監聽者

如果將工作移至個別事件監聽器呢?在一個事件監聽器中更新 UI,並在另一個監聽器中封鎖網頁。

查看完整程式碼:two_click.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

button
.addEventListener('click', () => {
  blockFor
(1000);
});

現在效能面板中會顯示什麼?

不同事件類型

大多數互動都會觸發多種事件,包括指標或按鍵事件、懸停、焦點/模糊,以及 beforechange 和 beforeinput 等合成事件。

許多實際網頁都有多個不同事件的監聽器。

如果變更事件監聽器的事件類型,會發生什麼情況?舉例來說,您是否要將其中一個 click 事件監聽器替換為 pointerupmouseup

查看完整程式碼:diff_handlers.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

button
.addEventListener('pointerup', () => {
  blockFor
(1000);
});

不更新 UI

如果從事件監聽器中移除更新 UI 的呼叫,會發生什麼情況?

查看完整程式碼:no_ui.html

button.addEventListener('click', () => {
  blockFor
(1000);
 
// score.incrementAndUpdateUI();
});

6. 處理時間實驗結果

效能追蹤:先更新 UI

查看完整程式碼:ui_first.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
  blockFor
(1000);
});

查看點選按鈕的「成效」面板記錄,您會發現結果並未變更。雖然在封鎖程式碼之前觸發了 UI 更新,但瀏覽器要等到事件監聽器完成後,才會實際更新繪製到螢幕上的內容,這表示互動仍需一秒以上才能完成。

「效能」面板中持續一秒的互動

效能追蹤記錄:分離式接聽器

查看完整程式碼:two_click.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

button
.addEventListener('click', () => {
  blockFor
(1000);
});

同樣地,這兩者在功能上並無不同。互動時間仍為一整秒。

如果將點擊互動放大,您會發現 click 事件確實會呼叫兩個不同的函式。

如預期,第一個 (更新 UI) 的執行速度非常快,第二個則需要整整一秒。但這些效果加總起來,最終仍會導致使用者互動緩慢。

這個範例中,互動時間為一秒,放大後可看到第一個函式呼叫完成時間不到一毫秒

效能追蹤:不同事件類型

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

button
.addEventListener('pointerup', () => {
  blockFor
(1000);
});

這些結果非常相似。互動時間仍為一秒,唯一的差別在於較短的 UI 更新專用 click 監聽器現在會在封鎖 pointerup 監聽器之後執行。

這個範例的互動時間為一秒,放大後可看到指標向上事件監聽器完成後,點擊事件監聽器不到一毫秒就完成。

效能追蹤:沒有 UI 更新

查看完整程式碼:no_ui.html

button.addEventListener('click', () => {
  blockFor
(1000);
 
// score.incrementAndUpdateUI();
});
  • 分數不會更新,但網頁仍會更新!
  • 動畫、CSS 效果、預設網頁元件動作 (表單輸入)、文字輸入、文字醒目顯示都會持續更新。

在這種情況下,按鈕會進入有效狀態,並在點選時返回,這需要瀏覽器繪製,表示仍有 INP。

由於事件監聽器封鎖了主要執行緒一秒,導致網頁無法繪製,因此互動仍需一秒。

錄製「效能」面板時,互動方式與先前幾乎相同。

「效能」面板中持續一秒的互動

重點

任何事件監聽器中執行的任何程式碼,都會延遲互動。

  • 包括從不同指令碼註冊的監聽器,以及在監聽器中執行的架構或程式庫程式碼,例如觸發元件算繪的狀態更新。
  • 不僅是您自己的程式碼,也包括所有第三方指令碼。

這是常見問題!

最後,程式碼不會觸發繪製,並不代表繪製不會等待緩慢的事件監聽器完成。

7. Experiment: input delay

如果事件監聽器以外的程式碼執行時間過長,會發生什麼情況?例如:

  • 如果載入時間較長的 <script> 在載入期間隨機封鎖網頁。
  • 定期封鎖網頁的 API 呼叫 (例如 setInterval)?

請嘗試從事件監聽器中移除 blockFor,並將其新增至 setInterval()

查看完整程式碼:input_delay.html

setInterval(() => {
  blockFor
(1000);
}, 3000);


button
.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

說明/活動

8. 輸入延遲實驗結果

查看完整程式碼:input_delay.html

setInterval(() => {
  blockFor
(1000);
}, 3000);


button
.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

如果在 setInterval 封鎖工作執行期間記錄按鈕點擊,即使互動本身沒有封鎖工作,也會導致長時間互動!

這類長時間執行的作業通常稱為「長時間工作」。

將滑鼠游標懸停在開發人員工具中的互動上,您會發現互動時間現在主要歸因於輸入延遲,而非處理時間。

開發人員工具的「效能」面板顯示一項封鎖工作 (1 秒)、該工作進行到一半時發生的互動,以及 642 毫秒的互動 (主要歸因於輸入延遲)

請注意,這不一定會影響互動!如果工作執行時沒有點選,或許可以順利完成。如果這類「隨機」打噴嚏只會偶爾造成問題,偵錯時可能會非常棘手。

如要找出這些問題,可以測量長時間工作 (或長時間動畫影格) 和總封鎖時間

9. 簡報速度緩慢

到目前為止,我們已透過輸入延遲或事件監聽器查看 JavaScript 的效能,但還有哪些因素會影響下一次算繪?

沒錯,就是用昂貴的特效更新頁面!

即使網頁更新速度很快,瀏覽器可能仍需費力轉譯!

在主執行緒上:

  • 需要在狀態變更後算繪更新內容的 UI 架構
  • DOM 變更或切換許多耗用資源的 CSS 查詢選擇器,可能會觸發大量樣式、版面配置和繪製作業。

在主執行緒外:

  • 使用 CSS 驅動 GPU 效果
  • 加入非常大的高解析度圖片
  • 使用 SVG/Canvas 繪製複雜場景

網頁上不同算繪元素的草圖

RenderingNG

以下是網路上常見的範例:

  • SPA 網站會在點選連結後重建整個 DOM,不會暫停以提供初始視覺回饋。
  • 搜尋頁面提供複雜的搜尋篩選器和動態使用者介面,但執行這些篩選器需要耗費大量監聽器。
  • 深色模式切換按鈕,可觸發整個頁面的樣式/版面配置

10. 實驗:回覆顯示延遲

requestAnimationFrame 速度緩慢

讓我們使用 requestAnimationFrame() API 模擬長時間的簡報延遲。

blockFor 呼叫移至 requestAnimationFrame 回呼中,以便在事件監聽器傳回後執行:

查看完整程式碼:presentation_delay.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
  requestAnimationFrame
(() => {
    blockFor
(1000);
 
});
});

說明/活動

11. 回覆顯示延遲實驗結果

查看完整程式碼:presentation_delay.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
  requestAnimationFrame
(() => {
    blockFor
(1000);
 
});
});

互動時間仍為一秒,那麼發生了什麼事?

requestAnimationFrame 會在下一次繪製之前要求回呼。由於 INP 測量的是從互動到下一個顯示的內容之間的時間,因此 blockFor(1000) 中的 requestAnimationFrame 會持續封鎖下一個顯示的內容整整一秒。

「效能」面板中持續一秒的互動

不過,請注意以下兩點:

  • 將滑鼠懸停在圖表上,您會發現所有互動時間現在都花在「呈現延遲」上,因為主執行緒封鎖發生在事件監聽器傳回之後。
  • 主執行緒活動的根源不再是點擊事件,而是「動畫影格觸發」。

12. 診斷互動

在這個測試頁面上,回應速度會以分數、計時器和計數器 UI 等方式呈現,非常直觀,但測試一般網頁時,回應速度的變化就比較不明顯。

如果互動時間過長,我們不一定能找出原因。可能的原因包括:

  • 輸入延遲?
  • 事件處理時間?
  • 簡報顯示延遲?

您可以在任何網頁上使用開發人員工具,協助評估回應式設計。如要養成習慣,請嘗試以下流程:

  1. 照常瀏覽網頁。
  2. 請留意開發人員工具「效能」面板即時指標檢視畫面中的「互動」記錄。
  3. 如果發現互動效果不佳,請嘗試重複操作:
  • 如果無法重複,請使用互動記錄取得洞察資料。
  • 如果可以重現,請在「效能」面板中記錄追蹤記錄。

所有延誤

請試著在頁面中加入一些上述問題:

查看完整程式碼:all_the_things.html

setInterval(() => {
  blockFor
(1000);
}, 3000);

button
.addEventListener('click', () => {
  blockFor
(1000);
  score
.incrementAndUpdateUI();

  requestAnimationFrame
(() => {
    blockFor
(1000);
 
});
});

接著使用控制台和效能面板診斷問題!

13. 實驗:非同步工作

由於您可以在互動中啟動非視覺效果 (例如發出網路要求、啟動計時器或只是更新全域狀態),如果這些效果最終更新了網頁,會發生什麼事?

只要允許在互動後進行下一次繪製,即使瀏覽器決定實際上不需要新的算繪更新,互動指標也會停止測量。

如要試用這項功能,請繼續從點擊事件監聽器更新 UI,但從逾時執行封鎖工作。

查看完整程式碼:timeout_100.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  setTimeout
(() => {
    blockFor
(1000);
 
}, 100);
});

接下來呢?

14. 非同步工作實驗結果

查看完整程式碼:timeout_100.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  setTimeout
(() => {
    blockFor
(1000);
 
}, 100);
});

27 毫秒的互動,以及 1 秒的長任務,現在會顯示在追蹤記錄的後段

由於 UI 更新後主執行緒會立即可用,因此互動時間現在很短。長時間的封鎖工作仍會執行,只是會在繪製後執行,因此使用者會立即收到 UI 回饋。

教訓:如果無法移除,至少要移動!

方法

我們是否可以做得比固定 100 毫秒的 setTimeout 更出色?我們可能仍希望程式碼盡快執行,否則就應該直接移除程式碼!

目標:

  • 互動會執行 incrementAndUpdateUI()
  • blockFor() 會盡快執行,但不會阻礙下一次繪製。
  • 這樣一來,系統就會以可預測的方式運作,不會發生「神奇逾時」的情況。

您可以透過下列方式達成此目標:

  • setTimeout(0)
  • Promise.then()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

「requestPostAnimationFrame」

與單獨使用 requestAnimationFrame 不同 (這會嘗試在下一次繪製前執行,通常仍會導致互動緩慢),requestAnimationFrame + setTimeout 可為 requestPostAnimationFrame 提供簡單的 Polyfill,在下一次繪製後執行回呼。

查看完整程式碼:raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame
(() => {
    setTimeout
(callback, 0);
 
});
}

button
.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  afterNextPaint
(() => {
    blockFor
(1000);
 
});
});

為了符合人體工學,您甚至可以將其包裝在 Promise 中:

查看完整程式碼:raf+task2.html

async function nextPaint() {
 
return new Promise(resolve => afterNextPaint(resolve));
}

button
.addEventListener('click', async () => {
  score
.incrementAndUpdateUI();

  await nextPaint
();
  blockFor
(1000);
});

15. 多次互動 (和暴怒點擊)

雖然將長時間阻斷工作移到其他地方有助於解決問題,但這些工作仍會阻斷網頁,影響日後的互動,以及許多其他網頁動畫和更新。

再次嘗試使用網頁的非同步封鎖工作版本 (或您在最後一個步驟中自行設計的延遲工作變體):

查看完整程式碼:timeout_100.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  setTimeout
(() => {
    blockFor
(1000);
 
}, 100);
});

如果快速點選多次會發生什麼情況?

效能追蹤

每次點擊都會將一秒長的作業排入佇列,確保主執行緒遭到封鎖相當長的時間。

主執行緒中有多個耗時一秒以上的作業,導致互動速度緩慢,最慢可達 800 毫秒

如果這些耗時的工作與新點擊重疊,即使事件監聽器本身幾乎會立即傳回,互動速度仍會變慢。我們已建立與先前輸入延遲實驗相同的情況。但這次的輸入延遲並非來自 setInterval,而是由先前的事件監聽器觸發的工作所致。

策略

理想情況下,我們希望完全移除長時間工作!

  • 完全移除不必要的程式碼,尤其是指令碼。
  • 最佳化程式碼,避免執行耗時的工作。
  • 在收到新的互動時中止過時的工作。

16. 策略 1:去抖動

這是經典策略。如果互動接踵而來,且處理或網路效果成本高昂,請刻意延遲啟動作業,以便取消並重新啟動。這種模式適用於自動完成欄位等使用者介面。

  • 使用 setTimeout 延遲啟動耗用資源的工作,並設定計時器 (例如 500 到 1000 毫秒)。
  • 請儲存計時器 ID。
  • 如果收到新的互動,請使用 clearTimeout 取消先前的計時器。

查看完整程式碼:debounce.html

let timer;
button
.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

 
if (timer) {
    clearTimeout
(timer);
 
}
  timer
= setTimeout(() => {
    blockFor
(1000);
 
}, 1000);
});

效能追蹤

多項互動,但最終只產生一項長時間的工作

即使多次點選,也只會執行一項 blockFor 工作,且會等待一整秒沒有任何點選動作後才執行。如果互動會以連發形式出現 (例如在文字輸入欄位中輸入內容,或預期項目目標會快速獲得多次點擊),建議預設使用這項策略。

17. 策略 2:中斷長時間執行的工作

但如果延遲時間結束後,又發生點擊事件,且該事件落在長時間工作的中間,就可能因輸入延遲而導致互動速度緩慢。

理想情況下,如果互動發生在工作期間,我們會暫停忙碌的工作,以便立即處理任何新的互動。如何達成這個目標?

有些 API (例如 isInputPending) 也能達到類似效果,但一般來說,最好將長時間執行的工作分割成多個區塊

大量 setTimeout

第一次嘗試:做簡單的事。

查看完整程式碼:small_tasks.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  requestAnimationFrame
(() => {
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
 
});
});

這項功能可讓瀏覽器個別排定每項工作,並優先處理輸入內容!

多項互動,但所有排定的工作都已細分成許多較小的任務

我們又回到五次點擊五秒的工作,但每次點擊的一秒工作已分成十個 100 毫秒的工作。因此,即使多個互動與這些工作重疊,任何互動的輸入延遲都不會超過 100 毫秒!瀏覽器會優先處理傳入的事件監聽器,而非 setTimeout 工作,因此互動仍能保持回應。

如果您要排定個別進入點 (例如在應用程式載入時呼叫大量獨立功能),這個策略特別實用。根據預設,只要載入指令碼並在指令碼評估時間執行所有項目,所有項目就會在巨大的長時間工作中執行。

不過,如果程式碼緊密耦合 (例如使用共用狀態的 for 迴圈),這種策略就無法有效拆解。

現在支援 yield()

不過,我們可以運用新版 asyncawait,輕鬆將「產生點」新增至任何 JavaScript 函式。

例如:

查看完整程式碼:yieldy.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldy(ms) {
  const ms_per_part = 10;
  const parts = ms / ms_per_part;
  for (let i = 0; i < parts; i++) {
    await schedulerDotYield();

    blockFor(ms_per_part);
  }
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();
  await blockInPiecesYieldy(1000);
});

與先前一樣,主執行緒會在完成一組工作後產生,瀏覽器也能回應任何傳入的互動,但現在只需要 await schedulerDotYield(),不必使用個別的 setTimeout,因此即使在 for 迴圈中也能輕鬆使用。

現在支援 AbortContoller()

這樣做雖然可行,但每次互動都會排定更多工作,即使有新的互動傳入,且可能改變需要完成的工作,也是如此。

使用去抖動策略時,我們會在每次新互動時取消先前的逾時。我們可以在這裡做類似的事情嗎?其中一種做法是使用 AbortController()

查看完整程式碼:aborty.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldyAborty(ms, signal) {
  const parts = ms / 10;
  for (let i = 0; i < parts; i++) {
    // If AbortController has been asked to stop, abandon the current loop.
    if (signal.aborted) return;

    await schedulerDotYield();

    blockFor(10);
  }
}

let abortController = new AbortController();

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  abortController.abort();
  abortController = new AbortController();

  await blockInPiecesYieldyAborty(1000, abortController.signal);
});

當點擊事件傳入時,系統會啟動 blockInPiecesYieldyAborty for 迴圈,執行需要完成的任何工作,同時定期產生主執行緒,讓瀏覽器持續回應新的互動。

當第二次點擊發生時,第一個迴圈會標示為已取消 (使用 AbortController),並啟動新的 blockInPiecesYieldyAborty 迴圈。下次排定再次執行第一個迴圈時,系統會注意到 signal.aborted 現在為 true,並立即傳回,不會執行後續工作。

主執行緒工作現在分成許多小部分,互動時間很短,工作時間也只會持續到必要為止

18. 結論

所有長時間執行的工作拆開,網站就能回應新的互動。這樣您就能快速提供初步意見,並決定是否要中止進行中的工作。有時這表示要將進入點排定為個別工作。有時這表示要在方便的位置新增「產生」點。

記住

  • INP 會評估所有互動。
  • 系統會測量從輸入到下一個顯示的內容之間的每次互動,也就是使用者看到回應的方式。
  • 輸入延遲、事件處理時間和呈現延遲都會影響互動回應情形。
  • 您可以使用開發人員工具輕鬆評估 INP 和互動細目!

策略

  • 網頁上沒有長時間執行的程式碼 (長時間執行的工作)。
  • 將不必要的程式碼移出事件監聽器,直到下一次繪製完成為止。
  • 確保瀏覽器能有效率地更新算繪。

瞭解詳情