INP 互動到下次繪製:取代 FID 的互動延遲優化
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. 切分長任務
把一個大任務切成好幾個小任務,中間讓瀏覽器有機會處理事件:
// 一次處理 10000 筆,主執行緒被佔用很久
function processAll(items) {
for (const item of items) {
heavyWork(item);
}
}// 分批處理,每批之間讓出主執行緒
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. 先回應,再做重活
使用者按下按鈕後,先更新畫面讓他知道有反應,重的工作延後:
// 所有事情都在同一個任務裡,畫面要等全部跑完才更新
button.addEventListener('click', () => {
updateUI(); // 使用者想立刻看到這個
saveToServer(); // 這個可以慢一點
recalculateAll(); // 這個更慢
});// 先繪製,重活排到下一個任務
button.addEventListener('click', async () => {
updateUI();
// 讓瀏覽器先把 updateUI 的結果畫出來
await new Promise((resolve) =>
requestAnimationFrame(() => setTimeout(resolve, 0)),
);
saveToServer();
recalculateAll();
});3. 避免強制同步版面重算
在同一個任務裡「改了樣式又立刻讀版面資訊」會強迫瀏覽器提前重算版面:
// 每一圈都強制重算一次版面
items.forEach((el) => {
el.style.width = '100px';
console.log(el.offsetHeight); // 讀取觸發重算
});// 先全部讀完,再全部寫
const heights = items.map((el) => el.offsetHeight);
items.forEach((el, i) => {
el.style.width = `${heights[i]}px`;
});會觸發重算的常見屬性
offsetWidth、offsetHeight、offsetTop、clientWidth、clientHeight、scrollTop、getBoundingClientRect()、getComputedStyle()。
規則是 先讀後寫,不要交錯。這與 CSS 動畫的效能 是同一類問題。
框架水合的影響
伺服器端渲染的頁面會經歷 水合(Hydration),把事件監聽掛回既有的 DOM。這段時間畫面看得到卻按不動。
水合是 INP 變差的常見原因
頁面內容愈多、元件愈複雜,水合的工作量愈大。大型頁面的水合可能是一個長達數百毫秒的長任務。
| 手法 | 說明 |
|---|---|
| 部分水合 | 只水合真正需要互動的區塊 |
| 島嶼架構 | 靜態內容不水合,互動元件各自獨立 |
| 延後水合 | 元件進入可視範圍或被互動時才水合 |
| 減少元件數 | 靜態內容不要包成元件 |
詳見 水合 Hydration 與 INP 。
第三方程式碼常是主因
它們跑在同一個主執行緒上
廣告、分析、客服元件、A/B 測試工具,這些程式碼的長任務一樣會擋住您的互動,而且您改不了它們的內容。
| 手法 | 說明 |
|---|---|
| 延後載入 | 用 defer 或在互動後才載入 |
| 條件載入 | 客服元件等使用者點了才載入 |
| 移到工作執行緒 | 部分分析工具支援 |
| 評估必要性 | 最有效的手法,通常也最難推動 |
<!-- 客服元件:點了按鈕才載入 -->
<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:
// 主執行緒:只負責派工與收結果
const worker = new Worker('/heavy-worker.js');
button.addEventListener('click', () => {
updateUI(); // 立刻回應
worker.postMessage({ items });
});
worker.onmessage = ({ data }) => {
renderResult(data);
};// heavy-worker.js:在另一個執行緒跑,不影響互動
self.onmessage = ({ data }) => {
const result = data.items.map(heavyWork);
self.postMessage(result);
};Worker 不能操作 DOM
它只能做純運算,結果傳回主執行緒才能更新畫面。所以適合的是「算完再畫」的場景,不適合需要頻繁操作 DOM 的工作。
怎麼量測
// 記錄每次互動的延遲與元素
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)比較接近真實情況。驗收仍要看 真實使用者數據 。
排查流程
- 用真實數據找出最慢的互動:Search Console 或自埋的量測會指出是哪個元素。
- 開 DevTools 的 Performance,開 CPU 節流:錄下那個互動。
- 看時間軸上的長任務:是誰擋住的?自己的程式還是第三方程式碼?
- 判斷是三段中的哪一段:輸入延遲(有長任務)、處理時間(處理函式太重)、還是呈現延遲(版面重算)。
- 對症下藥:切分長任務、先回應後做重活、或處理第三方程式碼。
檢查清單
| 項目 | 標準 |
|---|---|
| INP 在 200 毫秒內(第 75 百分位) | 目標 |
| 沒有超過 50 毫秒的長任務 | 建議 |
| 事件處理函式不做大量同步運算 | 建議 |
| 沒有讀寫交錯造成的強制版面重算 | 建議 |
| 第三方程式碼已延後或條件載入 | 建議 |
| 重運算搬到 Web Worker | 依需求 |
| 水合成本已評估 | 建議 |
| 用 CPU 節流測過 | 建議 |
常見問題
INP 和 FID 差在哪裡?
FID 只量第一次互動的等待時間,而且不計算事件處理與畫面更新的部分,容易低估問題。INP 觀察整個造訪期間的所有互動,取接近最差的那一次,並涵蓋從按下到畫面更新的完整過程。
什麼是長任務?
在主執行緒上連續執行超過 50 毫秒的 JavaScript。主執行緒同時負責執行程式與更新畫面,所以長任務執行期間任何互動都無法被處理,使用者按了就是沒反應。
為什麼互動了畫面卻沒更新?
事件處理函式可能已經跑完並改了狀態,但瀏覽器還沒有機會繪製。如果處理函式裡做了大量同步計算,或在同一個任務裡強制讀取版面資訊,繪製就會被推遲。
第三方程式碼會影響這個指標嗎?
會,而且常是主因。廣告、分析與客服元件都在同一個主執行緒上執行,它們的長任務一樣會擋住您的互動。處理方式是延後載入、改用 Web Worker,或評估是否真的需要那個元件。