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

1. 簡介

用於瞭解與下一個繪製動作 (INP) 互動的互動式示範和程式碼研究室。

描繪主執行緒互動的圖表。使用者在封鎖任務執行時輸入內容。輸入作業會延遲到這些工作完成之後,指標、滑鼠懸停和點擊事件監聽器會執行,然後開始轉譯和繪製工作,直到下一個影格顯示為止

必要條件

  • 具備 HTML 和 JavaScript 開發知識。
  • 建議做法:參閱 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/

應用程式總覽

頁面頂端會顯示「Score」計數器和「增加」按鈕。這是一款經典的示範,展現真力和反應能力!

本程式碼研究室的試用版應用程式螢幕截圖

按鈕下方有四個測量值:

  • INP:目前的 INP 分數,通常是最差的互動。
  • 互動:最近一次互動的分數。
  • FPS:網頁的主要執行緒每秒影格數。
  • 計時器:執行中的計時器動畫,以視覺化方式呈現卡頓。

測量互動完全不需使用 FPS 和計時器項目。新增這些註解的目的,是想更輕鬆地透過視覺化的方式呈現回應。

立即試用

請試著與「增加」按鈕互動,觀察分數提高的情況。INPInteraction 值每次的值是否增加?

INP 會評估從使用者互動開始,到網頁實際向使用者顯示轉譯的更新內容所花費的時間。

3. 運用 Chrome 開發人員工具評估互動情形

透過更多工具開啟開發人員工具開發人員工具選單:在頁面上按一下滑鼠右鍵並選取「檢查」,或者使用鍵盤快速鍵

切換至您要用於評估互動的「成效」面板。

開發人員工具效能面板和應用程式旁的螢幕截圖

接著,在效能面板中擷取互動資料。

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

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

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

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

開發人員工具效能面板的螢幕截圖、遊標懸停在面板中的互動上,以及列出互動短時間的工具提示

將滑鼠懸停在互動上方,即可看到互動速度快、沒有時間處理時間長度,以及輸入延遲顯示延遲時間的最短時間長度 (實際長度取決於機器的速度)。

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

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

查看完整程式碼:click_block.html

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

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

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

效能追蹤

在「效能」面板中拍攝另一個記錄,看看會如何。

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

曾經發生過短暫的互動,如今卻耗費一整秒,

將遊標懸停在互動上時,您會發現「Processing Duration」中幾乎都花費時間,也就是執行事件監聽器回呼所需的時間。由於封鎖的 blockFor 呼叫完全在事件監聽器內,因此就是如此。

5. 實驗:處理時間

嘗試重新調整事件事件監聽器的工作方式,看看對 INP 的影響。

請先更新 UI

如果替換 js 呼叫的順序,請先更新 UI,再進行封鎖。

查看完整程式碼:ui_first.html

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

您是否曾注意到 UI 較早出現?這種順序是否會影響 INP 分數?

請追蹤記錄並檢查互動情況,看看是否有任何差異。

分隔事件監聽器

如果將工作移至另一個事件監聽器,該怎麼做?在單一事件監聽器中更新使用者介面,然後從個別事件監聽器封鎖網頁。

查看完整程式碼:two_click.html

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

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

成效面板現在的外觀如何?

不同事件類型

多數互動會觸發多種事件,包括指標或重要事件、懸停、聚焦/模糊處理,以及合成事件 (例如變更前和輸入前)。

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

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

查看完整程式碼:diff_handlers.html

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

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

無使用者介面更新

如果從事件監聽器移除更新 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 事件監聽器之後執行。

深入查看這個範例中的一秒互動情形,顯示指標事件監聽器之後,完成點擊事件監聽器的時間不到一毫秒。

效能追蹤:無使用者介面更新

查看完整程式碼:no_ui.html

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

在此情況下,按鈕會進入正常狀態,並在使用者按下按鈕後返回,因此瀏覽器必須繪製,這表示仍有 INP 值。

由於事件監聽器封鎖主執行緒一秒,因而無法繪製網頁,因此互動仍需要一整秒的時間。

使用效能面板錄製畫面,就能看出互動過程與先前的互動情形,幾乎完全相同。

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

重點摘要

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

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

這真的是很常見的問題!

最後:即使程式碼不會觸發繪製動作,也不表示在執行速度緩慢的事件監聽器中完成繪製作業。

7. 實驗:輸入延遲

在事件監聽器外長時間執行程式碼呢?例如:

  • 如果延遲載入 <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 封鎖工作執行時發生的按鈕點擊,導致長時間執行的互動,即使互動本身沒有執行任何封鎖工作也一樣!

這些長時間執行的時間通常稱為長時間任務。

將滑鼠遊標懸停在開發人員工具中的互動事件上,就會看到互動時間現在主要歸因於輸入延遲,而非處理時間長度。

開發人員工具效能面板顯示一秒的封鎖工作、透過該工作中一部分的互動,以及 642 毫秒的互動,主要歸因於輸入延遲

注意,互動次數並「不」會影響互動!如果在執行工作時沒有點按滑鼠,可能會得到幸運的幸運轉蛋。這類「隨機」打噴嚏可能只是個惡夢,但有時候它只會造成問題。

如要追蹤這些事件,其中一種方法是測量長時間工作 (或長動畫影格) 和「Total Blocking Time」。

9. 簡報速度緩慢

到目前為止,我們已經查看過 JavaScript 透過輸入延遲或事件監聽器的效能,不過還有哪些因素會影響系統接下來繪製的繪製作業?

嗯,用昂貴的效果更新頁面!

因此,即使網頁更新速度很快,瀏覽器可能還是會持續轉譯網頁!

在主執行緒上:

  • 需要在狀態變更後轉譯更新的 UI 架構
  • DOM 改變,或切換多個昂貴的 CSS 查詢選取器,都會觸發許多樣式、版面配置和 Paint。

關閉主執行緒:

  • 使用 CSS 支援 GPU 效果
  • 新增超大型的高解析度圖片
  • 使用 SVG/畫布繪製複雜的場景

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

RenderingNG

以下列舉一些常見的網路例子:

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

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 測量的是互動到下一次顏料所需的時間,因此 requestAnimationFrame 中的 blockFor(1000) 會繼續封鎖下一幅顏料 1 秒。

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

但請注意以下兩點:

  • 將滑鼠懸停在「顯示延遲」中,就會顯示所有互動時間因為在事件監聽器回傳後,會發生主執行緒的封鎖作業。
  • 主執行緒活動的根不再是點擊事件,而是「Animation Frame 已觸發」。

12. 診斷互動

在這個測試頁面中,回應速度非常視覺化,包含分數、計時器和計數器 UI,但是測試平均網頁時比較細膩。

如果互動時間很長,您不一定能清楚找出問題所在。是:

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

如有需要,您可以在任何頁面上使用開發人員工具,協助評估回應速度。養成習慣,嘗試以下流程:

  1. 按照平常的方式瀏覽網路。
  2. 選用:在 Web Vitals 擴充功能記錄互動時,將開發人員工具控制台保持開啟。
  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 毫秒互動,目前於追蹤記錄中發生

主執行緒在 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);
  });
});

針對人體工學,您甚至可以包裝在承諾中:

查看完整程式碼: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);
  });
});

運作方式是允許瀏覽器個別排定每項工作,輸入內容的優先順序較高!

有多個互動,但所有排定的工作已拆分為許多較小的工作。

我們當時進行了 5 次點擊的工作,現在則恢復到 5 秒的全貌。現在,每次點擊一秒工作都分割成 10 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. 結語

只要將「所有」的長時間任務分割為不同時間,網站就能回應新互動。這可讓您快速提供初步意見回饋,也可讓您做決定 (例如取消進行中的作業)。有時也就是將進入點安排為個別的工作。有時只要加上「yield」便利貼

記住

  • INP 會評估所有互動。
  • 每次互動都是從輸入到下一次繪製開始測量,也就是使用者「看到」回應率的方式。
  • 輸入延遲、事件處理時長和顯示延遲,皆「全部」會影響互動反應。
  • 您可以使用開發人員工具輕鬆評估 INP 和互動的細目!

策略

  • 不要在網頁上執行長時間執行的程式碼 (長時間工作)。
  • 將無用的程式碼移出事件監聽器,直到下次繪製為止。
  • 確保轉譯更新能對瀏覽器有效。

瞭解詳情