1. 簡介
用於瞭解與下一個繪製動作 (INP) 互動的互動式示範和程式碼研究室。
必要條件
- 具備 HTML 和 JavaScript 開發知識。
- 建議做法:參閱 INP 說明文件。
課程內容
- 使用者互動的交互作用以及您處理這些互動的方式,對網頁回應速度有何影響。
- 如何減少及排除延遲,提供流暢的使用者體驗。
需求條件
- 一台能夠從 GitHub 複製程式碼及執行 npm 指令的電腦。
- 文字編輯器。
- 新版 Chrome 支援所有互動測量功能。
2. 做好準備
取得並執行程式碼
您可以在 web-vitals-codelabs
存放區中找到這個程式碼。
- 複製終端機中的存放區:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
- 瀏覽到複製的目錄:
cd web-vitals-codelabs/understanding-inp
- 安裝依附元件:
npm ci
- 啟動網路伺服器:
npm run start
- 透過瀏覽器造訪 http://localhost:5173/understanding-inp/
應用程式總覽
頁面頂端會顯示「Score」計數器和「增加」按鈕。這是一款經典的示範,展現真力和反應能力!
按鈕下方有四個測量值:
- INP:目前的 INP 分數,通常是最差的互動。
- 互動:最近一次互動的分數。
- FPS:網頁的主要執行緒每秒影格數。
- 計時器:執行中的計時器動畫,以視覺化方式呈現卡頓。
測量互動完全不需使用 FPS 和計時器項目。新增這些註解的目的,是想更輕鬆地透過視覺化的方式呈現回應。
立即試用
請試著與「增加」按鈕互動,觀察分數提高的情況。INP 和 Interaction 值每次的值是否增加?
INP 會評估從使用者互動開始,到網頁實際向使用者顯示轉譯的更新內容所花費的時間。
3. 運用 Chrome 開發人員工具評估互動情形
透過更多工具開啟開發人員工具開發人員工具選單:在頁面上按一下滑鼠右鍵並選取「檢查」,或者使用鍵盤快速鍵。
切換至您要用於評估互動的「成效」面板。
接著,在效能面板中擷取互動資料。
- 按下 [錄製]。
- 與頁面互動 (按下「增加」按鈕)。
- 停止錄製。
畫面上顯示的時間軸會顯示「互動」軌跡。按一下左側的三角形即可展開。
系統會顯示兩項互動。捲動或按住 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
事件監聽器替換為 pointerup
或 mouseup
嗎?
查看完整程式碼: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
封鎖工作執行時發生的按鈕點擊,導致長時間執行的互動,即使互動本身沒有執行任何封鎖工作也一樣!
這些長時間執行的時間通常稱為長時間任務。
將滑鼠遊標懸停在開發人員工具中的互動事件上,就會看到互動時間現在主要歸因於輸入延遲,而非處理時間長度。
注意,互動次數並「不」會影響互動!如果在執行工作時沒有點按滑鼠,可能會得到幸運的幸運轉蛋。這類「隨機」打噴嚏可能只是個惡夢,但有時候它只會造成問題。
如要追蹤這些事件,其中一種方法是測量長時間工作 (或長動畫影格) 和「Total Blocking Time」。
9. 簡報速度緩慢
到目前為止,我們已經查看過 JavaScript 透過輸入延遲或事件監聽器的效能,不過還有哪些因素會影響系統接下來繪製的繪製作業?
嗯,用昂貴的效果更新頁面!
因此,即使網頁更新速度很快,瀏覽器可能還是會持續轉譯網頁!
在主執行緒上:
- 需要在狀態變更後轉譯更新的 UI 架構
- DOM 改變,或切換多個昂貴的 CSS 查詢選取器,都會觸發許多樣式、版面配置和 Paint。
關閉主執行緒:
- 使用 CSS 支援 GPU 效果
- 新增超大型的高解析度圖片
- 使用 SVG/畫布繪製複雜的場景
以下列舉一些常見的網路例子:
- 會在點選連結後重新建構整個 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,但是測試平均網頁時比較細膩。
如果互動時間很長,您不一定能清楚找出問題所在。是:
- 輸入延遲?
- 事件處理時間?
- 簡報顯示延遲?
如有需要,您可以在任何頁面上使用開發人員工具,協助評估回應速度。養成習慣,嘗試以下流程:
- 按照平常的方式瀏覽網路。
- 選用:在 Web Vitals 擴充功能記錄互動時,將開發人員工具控制台保持開啟。
- 如果發現互動情形不佳,請嘗試重複執行該動作:
- 如果無法重複出現,請使用控制台記錄取得深入分析資訊。
- 如要重複錄製,請在成效面板中錄製。
所有延誤狀況
請嘗試在網頁上加入以下這些問題:
查看完整程式碼: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);
});
主執行緒在 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);
});
如果您快速點選多次,會發生什麼情況?
效能追蹤
在每個點擊中,都有一個耗時一秒的工作排入佇列,確保主執行緒已長時間封鎖。
如果這些長時間工作與來自新點擊的時間重疊,即使事件監聽器本身幾乎立即傳回,仍會造成互動速度緩慢。我們建立的情況與先前實驗中的輸入延遲相同,目前只有這個情況,輸入延遲時間並非來自 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()
」觀看
不過,我們可以利用新型 async
和 await
輕鬆新增「收益點」加到任何 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 和互動的細目!
策略
- 不要在網頁上執行長時間執行的程式碼 (長時間工作)。
- 將無用的程式碼移出事件監聽器,直到下次繪製為止。
- 確保轉譯更新能對瀏覽器有效。