Skip to content

INP 互動到下次繪製:取代 FID 的互動延遲優化

INP 互動到下次繪製:互動延遲的優化

INP(Interaction to Next Paint,互動到下次繪製)回答的是:「我點了為什麼沒反應?」

評級門檻
良好≤ 200 毫秒
待改善200 - 500 毫秒
不佳> 500 毫秒

它在 2024 年正式取代 FID,成為 三個核心指標 中負責互動的那一項。

和 FID 的差別

FID(已退役)INP
量幾次互動只有第一次整個造訪期間的所有互動
取哪一個值那唯一一次接近最差的那一次
涵蓋範圍只有輸入延遲輸入延遲 + 處理 + 繪製
容易低估問題

為什麼要換

FID 只量「按下去到開始處理」那一段。一個網站可能第一次互動很快(拿到好分數),但之後每次點擊都卡三秒,FID 完全反映不出來。

INP 涵蓋 從按下到畫面真的更新 的完整過程,也涵蓋整個造訪期間,所以更貼近使用者的真實感受。

哪些算「互動」

不算
點擊(滑鼠、觸控)滑動捲軸
鍵盤按鍵滑鼠移動
觸控點按縮放

捲動不算互動

這常讓人意外。捲動由瀏覽器的合成執行緒處理,不需要等主執行緒,所以不列入 INP。

但捲動時觸發的 JavaScript(例如:無限滾動的載入)會佔用主執行緒,間接影響之後的互動。

把延遲拆成三段

階段從哪到哪慢的原因
1. 輸入延遲使用者按下到事件開始處理主執行緒正在跑別的長任務
2. 處理時間事件處理函式執行處理函式裡做了太多事
3. 呈現延遲處理完到畫面更新版面重算、繪製成本高

第 1 段最常是主因

使用者按下按鈕時,主執行緒可能正在執行一個 300 毫秒的任務,事件必須排隊等它跑完。這時候問題不在您的點擊處理函式,而在 那個擋住主執行緒的任務

主執行緒為什麼會擋住互動

主執行緒同時負責執行程式與更新畫面

JavaScript 是單執行緒的。當它在跑一段程式時,畫面無法更新,事件無法處理

連續執行超過 50 毫秒 的任務叫做 長任務(Long Task)。長任務執行期間,使用者按了就是沒反應。

三個實作手法

1. 切分長任務

把一個大任務切成好幾個小任務,中間讓瀏覽器有機會處理事件:

js
// 一次處理 10000 筆,主執行緒被佔用很久
function processAll(items) {
  for (const item of items) {
    heavyWork(item);
  }
}
js
// 分批處理,每批之間讓出主執行緒
async function processAll(items) {
  const CHUNK = 100;
  for (let i = 0; i < items.length; i += CHUNK) {
    for (const item of items.slice(i, i + CHUNK)) {
      heavyWork(item);
    }
    await yieldToMain(); // 讓瀏覽器處理待辦的事件與繪製
  }
}

function yieldToMain() {
  // 支援的環境優先用排程 API
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }
  return new Promise((resolve) => setTimeout(resolve, 0));
}

scheduler.yield 比 setTimeout 好

setTimeout(0) 會把剩下的工作排到任務佇列的最後面,可能被其他任務插隊。scheduler.yield() 讓出後會優先接回自己的工作,同時仍給互動事件插隊的機會。

不支援時退回 setTimeout 也可以接受。

2. 先回應,再做重活

使用者按下按鈕後,先更新畫面讓他知道有反應,重的工作延後:

js
// 所有事情都在同一個任務裡,畫面要等全部跑完才更新
button.addEventListener('click', () => {
  updateUI(); // 使用者想立刻看到這個
  saveToServer(); // 這個可以慢一點
  recalculateAll(); // 這個更慢
});
js
// 先繪製,重活排到下一個任務
button.addEventListener('click', async () => {
  updateUI();

  // 讓瀏覽器先把 updateUI 的結果畫出來
  await new Promise((resolve) =>
    requestAnimationFrame(() => setTimeout(resolve, 0)),
  );

  saveToServer();
  recalculateAll();
});

3. 避免強制同步版面重算

在同一個任務裡「改了樣式又立刻讀版面資訊」會強迫瀏覽器提前重算版面:

js
// 每一圈都強制重算一次版面
items.forEach((el) => {
  el.style.width = '100px';
  console.log(el.offsetHeight); // 讀取觸發重算
});
js
// 先全部讀完,再全部寫
const heights = items.map((el) => el.offsetHeight);
items.forEach((el, i) => {
  el.style.width = `${heights[i]}px`;
});

會觸發重算的常見屬性

offsetWidthoffsetHeightoffsetTopclientWidthclientHeightscrollTopgetBoundingClientRect()getComputedStyle()

規則是 先讀後寫,不要交錯。這與 CSS 動畫的效能 是同一類問題。

框架水合的影響

伺服器端渲染的頁面會經歷 水合(Hydration),把事件監聽掛回既有的 DOM。這段時間畫面看得到卻按不動。

水合是 INP 變差的常見原因

頁面內容愈多、元件愈複雜,水合的工作量愈大。大型頁面的水合可能是一個長達數百毫秒的長任務。

手法說明
部分水合只水合真正需要互動的區塊
島嶼架構靜態內容不水合,互動元件各自獨立
延後水合元件進入可視範圍或被互動時才水合
減少元件數靜態內容不要包成元件

詳見 水合 Hydration 與 INP

第三方程式碼常是主因

它們跑在同一個主執行緒上

廣告、分析、客服元件、A/B 測試工具,這些程式碼的長任務一樣會擋住您的互動,而且您改不了它們的內容。

手法說明
延後載入defer 或在互動後才載入
條件載入客服元件等使用者點了才載入
移到工作執行緒部分分析工具支援
評估必要性最有效的手法,通常也最難推動
html
<!-- 客服元件:點了按鈕才載入 -->
<button id="open-chat">聯絡客服</button>
<script>
  document.getElementById("open-chat").addEventListener(
    "click",
    () => {
      const s = document.createElement("script");
      s.src = "https://third-party.example.com/chat.js";
      document.head.appendChild(s);
    },
    { once: true },
  );
</script>

把運算搬離主執行緒

真正吃 CPU 的工作(大量資料處理、圖片處理、加密)用 Web Worker:

js
// 主執行緒:只負責派工與收結果
const worker = new Worker('/heavy-worker.js');

button.addEventListener('click', () => {
  updateUI(); // 立刻回應
  worker.postMessage({ items });
});

worker.onmessage = ({ data }) => {
  renderResult(data);
};
js
// heavy-worker.js:在另一個執行緒跑,不影響互動
self.onmessage = ({ data }) => {
  const result = data.items.map(heavyWork);
  self.postMessage(result);
};

Worker 不能操作 DOM

它只能做純運算,結果傳回主執行緒才能更新畫面。所以適合的是「算完再畫」的場景,不適合需要頻繁操作 DOM 的工作。

怎麼量測

js
// 記錄每次互動的延遲與元素
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.interactionId) {
      console.log(
        '互動延遲:',
        entry.duration,
        '類型:',
        entry.name,
        '元素:',
        entry.target,
      );
    }
  }
}).observe({ type: 'event', buffered: true, durationThreshold: 40 });

本機很難重現真實的 INP

本機的裝置通常比使用者的手機快得多,長任務在本機可能只有 30 毫秒、在中階手機上卻是 300 毫秒。

用 DevTools 的 CPU 節流(4x 或 6x slowdown)比較接近真實情況。驗收仍要看 真實使用者數據

排查流程

  1. 用真實數據找出最慢的互動:Search Console 或自埋的量測會指出是哪個元素。
  2. 開 DevTools 的 Performance,開 CPU 節流:錄下那個互動。
  3. 看時間軸上的長任務:是誰擋住的?自己的程式還是第三方程式碼?
  4. 判斷是三段中的哪一段:輸入延遲(有長任務)、處理時間(處理函式太重)、還是呈現延遲(版面重算)。
  5. 對症下藥:切分長任務、先回應後做重活、或處理第三方程式碼。

檢查清單

項目標準
INP 在 200 毫秒內(第 75 百分位)目標
沒有超過 50 毫秒的長任務建議
事件處理函式不做大量同步運算建議
沒有讀寫交錯造成的強制版面重算建議
第三方程式碼已延後或條件載入建議
重運算搬到 Web Worker依需求
水合成本已評估建議
用 CPU 節流測過建議

常見問題

INP 和 FID 差在哪裡?

FID 只量第一次互動的等待時間,而且不計算事件處理與畫面更新的部分,容易低估問題。INP 觀察整個造訪期間的所有互動,取接近最差的那一次,並涵蓋從按下到畫面更新的完整過程。

什麼是長任務?

在主執行緒上連續執行超過 50 毫秒的 JavaScript。主執行緒同時負責執行程式與更新畫面,所以長任務執行期間任何互動都無法被處理,使用者按了就是沒反應。

為什麼互動了畫面卻沒更新?

事件處理函式可能已經跑完並改了狀態,但瀏覽器還沒有機會繪製。如果處理函式裡做了大量同步計算,或在同一個任務裡強制讀取版面資訊,繪製就會被推遲。

這個指標為什麼留到最後修?

因為它往往需要調整程式架構,成本比另外兩個指標高。版面偏移 多半只要補上尺寸,載入速度 多半只要處理圖片,但互動延遲常牽涉到把大型元件拆開、把運算搬離主執行緒這類改動。

第三方程式碼會影響這個指標嗎?

會,而且常是主因。廣告、分析與客服元件都在同一個主執行緒上執行,它們的長任務一樣會擋住您的互動。處理方式是延後載入、改用 Web Worker,或評估是否真的需要那個元件。

延伸閱讀

參考資料:web.dev:Interaction to Next Paint (INP)