Skip to content

2026-08-04

/ 13 分鐘閱讀

/ Web Performance Fundamentals

改善 INP:把工作切開,讓出主執行緒

INP 篇提過,改善 INP 的核心手段是透過非同步操作讓主執行緒盡快釋出。本篇以一個電商網站的「加入購物車」按鈕為例,實際示範這件事怎麼做。

情境:一個卡住 1200 毫秒的加入購物車按鈕

假設有一個電商網站,商品格線上每個商品都有一顆「Add to Cart(加入購物車)」按鈕。使用者點擊後,網站要做三件事:記錄這次點擊的分析追蹤資料、透過 fetch 呼叫後端把商品加入購物車、以及在完成後更新畫面上的購物車數量。

實際量測這顆按鈕的互動,發現使用者點擊後畫面沒有任何立即回饋,要等上一陣子,購物車數量才會更新。用瀏覽器開發工具量測這次點擊的 INP,得到的結果是 1200 毫秒,遠遠超過 Google 「良好」門檻(200 毫秒以內),屬於非常差的分數。

原本的點擊處理器程式碼很單純:從按鈕的 data-product-id 屬性取出商品 ID,接著同步執行一段分析追蹤程式碼,然後 await 一個加入購物車的 fetch 請求。問題出在中間那段分析追蹤程式碼:它是同步執行的,而且跑得非常久,久到它整段跑完之後,瀏覽器才有機會繪製下一幀畫面。換句話說,使用者點擊按鈕後,必須等這段昂貴的分析追蹤運算全部執行完畢,才會看到任何視覺回饋,這就是 INP 高達 1200 毫秒的原因。分析追蹤本身是必要功能,不需要拿掉,但它不應該用「擋住畫面繪製」的方式執行。

核心觀念:yield 就是讓出主執行緒

主執行緒只有一個,同時負責處理所有 JavaScript、事件與版面計算。「yield(讓出)」聽起來是個高深的詞,實際上做法很單純:把工作往後排,先讓瀏覽器有機會去做其他事(包括繪製下一幀)。

有兩種常見手法:

  • setTimeout:最古老、最熟悉的做法,把程式碼排到未來某個時間點執行,讓其他工作可以先跑
  • requestAnimationFrame:可以視為更精準版的 setTimeout,它會在瀏覽器「下一次即將繪製畫面之前」呼叫回呼函式。這個 API 原本是為了動畫設計的(例如高更新頻率的遊戲畫面),但對管理 INP 同樣重要,因為它讓我們能精確控制「主執行緒工作」跟「下一次繪製」之間的時間點

重寫後的處理邏輯

改善後的點擊處理器分成好幾層:

  1. 先做便宜的操作:從屬性取出商品 ID,這幾乎不花時間,不需要 yield
  2. 呼叫 requestAnimationFrame,把接下來要做的事都排到「瀏覽器準備繪製下一幀」的那一刻才執行,暫停一切處理
  3. 到了下一幀繪製前,做使用者看得到的回饋:把按鈕文字改成「Added」,並停用按鈕。這一步刻意做得很輕量,因為這就是 INP 要量測的「P(Paint)」發生的地方,不希望在這裡塞入昂貴運算
  4. setTimeoutupdateAnalytics 這段昂貴運算再往後推一層,確保它不會擋在繪製之前
  5. 加入購物車的 await 呼叫本身是非同步的,等同已經自動讓出主執行緒
  6. 再用一個 setTimeout(約 1500 毫秒後)把按鈕文字改回「Add to Cart」並移除 disabled 屬性,讓使用者可以再次點擊;這一層單純是等待動畫效果的時間差,不是為了讓出主執行緒

一個原本很小的函式,因為加了好幾層 yield 而變大了不少,但這種「包裝層」正是事件處理器改善 INP 時常見的樣子。

用執行順序來看,click 事件觸發後,點擊處理器現在只做一件很輕的事:呼叫 requestAnimationFrame,然後立刻結束,把主執行緒交還給瀏覽器。等到瀏覽器準備繪製下一幀時,才會呼叫剛剛排定的回呼函式,去改按鈕內容,並在裡面排入 setTimeoutaddToCart,接著再次把主執行緒讓出來,讓繪製真正發生。INP 到這裡就已經測量完畢,因為使用者的互動與瀏覽器繪製之間,只做了改按鈕文字這件輕量的事。至於 updateAnalytics 這段昂貴運算,要等到之後計時器觸發才會執行,但那已經是繪製完成之後的事,不再計入 INP。

實際效果

套用這個寫法後,使用者點擊按鈕會立刻看到按鈕文字變成「Added」,回饋明顯更即時。重新量測這次互動的 INP,時間從原本的 1200 毫秒大幅縮短到僅剩幾毫秒等級,遠低於 Google「良好」的 200 毫秒門檻。

值得注意的是:JavaScript 執行的總工作量並沒有減少updateAnalytics 依然是那個跑幾萬次迴圈的昂貴運算,執行時間長到 DevTools 會標示為「long task」警告。改善的地方只在於這段昂貴運算不再擋在點擊與繪製之間,INP 因此大幅改善。如果要讓這段運算本身變快,那是另一個層次的問題,需要拆解、優化實際的 JavaScript 邏輯(這類技巧屬於另一門課程的範疇)。就 INP 而言,關鍵永遠只有一件事:不要擋住主執行緒的繪製時機。

複習

為什麼原本的加入購物車按鈕 INP 高達 1200 毫秒?

因為點擊處理器在繪製下一幀之前,同步執行了一段耗時很長的分析追蹤程式碼(updateAnalytics),把繪製時機硬生生往後拖延。

requestAnimationFrame 在這個改善方案中扮演什麼角色?

它把使用者可見的回饋(改變按鈕文字、停用按鈕)安排在瀏覽器準備繪製下一幀的時間點執行,並把這一步跟繪製之前不必要的工作隔開。

為什麼要用 setTimeout 包住 updateAnalytics

因為這段運算很昂貴,如果留在繪製之前執行會拖慢 INP;用 setTimeout 排到之後執行,可以讓繪製先完成,INP 測量結束後這段運算再慢也不影響分數。

改善後,updateAnalytics 實際執行的時間變短了嗎?

沒有,它依然是同樣昂貴的運算,DevTools 甚至會標示為 long task。改變的只是它的執行時機被移到繪製之後,不再阻擋 INP。

這個改善方案背後唯一的核心目標是什麼?

不要讓任何工作擋在使用者互動與瀏覽器繪製下一幀之間,也就是持續為主執行緒讓路。

小測驗

原本的加入購物車按鈕,INP 為什麼高達 1200 毫秒? 點擊處理器在繪製前同步執行了耗時很長的分析追蹤程式碼
requestAnimationFrame 在改善方案中的作用是什麼? 把使用者可見的回饋安排在瀏覽器準備繪製下一幀時執行
為什麼要用 setTimeout 延後執行 updateAnalytics? 避免這段昂貴運算擋在繪製之前,拖慢 INP
改善後,updateAnalytics 本身的執行時間有變短嗎? 沒有,只是執行時機被移到繪製之後
改善 INP 唯一的核心目標是什麼? 不讓任何工作擋在互動與下一幀繪製之間

此文章是 FrontendMasters 上的 Web Performance Fundamentals 課程筆記

最後更新時間:

Buy Me A Coffee

系列章節 第 41 篇 / 共 41 篇

0 %
MIT Licensed | Copyright © 2025-present Wen-Hsiu's Blog
Photo by Federica Galli on Unsplash