標準繪製和回應指標在初始網頁載入後會留下測量差距:
LargestContentfulPaint(largest-contentful-paint) 只會評估初始 (「硬」) 文件載入,並在使用者首次與網頁互動時停止發出項目。PerformanceEventTiming和event/Interaction to Next Paint (INP) 會測量輸入回應速度,直到下一個影格 (例如按鈕啟用狀態或載入旋轉圖示),但不會追蹤後續的非同步繪製,例如在fetch()解析後算繪的新內容,或用戶端路徑轉換完成。- 在框架程式碼之外,可能難以觀察單頁應用程式 (SPA) 的路徑變更。如果只監控網址更新,可能會過度回報與路徑變更無關的網址變更 (例如錨點連結變更、單一路徑變更的多個「重新導向」)。
InteractionContentfulPaint (interaction-contentful-paint) 和 PerformanceSoftNavigation (soft-navigation) 效能項目類型可填補這項缺口。這類 API 會在非同步工作 (fetch、await、setTimeout) 之間傳播互動因果關係,將新的內容繪製和單頁應用程式 (SPA) 路由轉換,直接歸因於觸發這些事件的使用者互動。
導入方式
1. 觀察互動觸發的內容繪製
搭配使用 PerformanceObserver、type: 'interaction-contentful-paint' 和 buffered: true,觀察因使用者互動而逐漸增加的最大內容繪製。
每個 InteractionContentfulPaint 項目都會提供:
interactionId:啟動使用者互動的專屬 ID (與PerformanceEventTiming.interactionId和PerformanceSoftNavigation.interactionId相符)。startTime:啟動使用者互動開始的時間戳記,相對於初始硬導覽時間原點。paintTime和presentationTime:算繪階段結束的時間,以及在螢幕上繪製像素的時間。duration:從互動開始到繪製呈現 (presentationTime - startTime) 的經過時間。largestContentfulPaint:快照LargestContentfulPaint物件,包含該互動目前最大繪製元素 (element、size、url、id、renderTime、loadTime) 的詳細資料。
/**
* Observes contentful paints caused by user interactions and invokes a callback
* whenever an interaction produces a larger contentful paint.
*
* @param {(entry: PerformanceEntry) => void} onInteractionPaint
*/
export function observeInteractionPaints(onInteractionPaint) {
// Feature-detect support before calling observe() so unsupported browsers
// do not throw or log console warnings.
if (
typeof PerformanceObserver === 'undefined' ||
!PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint')
) {
return null;
}
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.duration is already calculated as (entry.presentationTime - entry.startTime),
// giving the total time from the user interaction to this contentful paint.
onInteractionPaint(entry);
}
});
// MANDATORY: Pass buffered: true to capture interaction paints that occurred
// before this observer was registered.
observer.observe({ type: 'interaction-contentful-paint', buffered: true });
return observer;
}
2. 評估 SPA 軟導覽的 Largest Contentful Paint (LCP)
當可信的使用者互動導致可見網址變更和內容繪製時,瀏覽器就會發出軟導覽。如要計算每次軟導覽的 LCP,請按照下列步驟操作:
- 使用
buffered: true觀察'soft-navigation'和'interaction-contentful-paint'項目。 - 必要:收到
'soft-navigation'項目時,請呼叫softNavEntry.getLargestInteractionContentfulPaint(),為初始 LCP 候選項目提供種子。如果應用程式在更新網址「之前」繪製內容 (例如算繪檢視畫面,然後呼叫history.pushState()),這些早期的interaction-contentful-paint項目會在soft-navigation項目之前發出,且仍會包含「先前」的navigationId。 - 必要:請比對
entry.interactionId === softNavEntry.interactionId,將後續的'interaction-contentful-paint'項目與有效的軟導覽建立關聯,請勿比對entry.navigationId。使用interactionId可確保納入網址更新前發生的繪製,並排除新路徑上後續不相關互動的繪製。
/**
* Tracks FCP and LCP for each client-side soft navigation and reports finalized
* route metrics when the user navigates to a new route or hides the page.
*
* @param {(report: {
* url: string,
* navigationId: number,
* interactionId: number,
* fcpDuration: number,
* lcpDuration: number | null,
* lcpElement: Element | null,
* lcpSize: number | null
* }) => void} onSoftNavReport
*/
export function trackSoftNavigationLCP(onSoftNavReport) {
const supported =
typeof PerformanceObserver !== 'undefined' &&
PerformanceObserver.supportedEntryTypes?.includes('soft-navigation') &&
PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint');
if (!supported) {
return null;
}
let activeSoftNav = null;
let activeLcpEntry = null;
function finalizeCurrentSoftNav() {
if (!activeSoftNav) return;
onSoftNavReport({
// activeSoftNav.name holds the destination URL of the soft navigation.
url: activeSoftNav.name,
navigationId: activeSoftNav.navigationId,
interactionId: activeSoftNav.interactionId,
// activeSoftNav.duration is (activeSoftNav.presentationTime - activeSoftNav.startTime),
// which represents First Contentful Paint (FCP) relative to interaction start.
fcpDuration: activeSoftNav.duration,
// activeLcpEntry.duration is (activeLcpEntry.presentationTime - activeSoftNav.startTime),
// representing the soft navigation's LCP duration relative to interaction start.
lcpDuration: activeLcpEntry ? activeLcpEntry.duration : null,
lcpElement: activeLcpEntry?.largestContentfulPaint?.element ?? null,
lcpSize: activeLcpEntry?.largestContentfulPaint?.size ?? null,
});
activeSoftNav = null;
activeLcpEntry = null;
}
const softNavObserver = new PerformanceObserver((list) => {
for (const softNavEntry of list.getEntries()) {
// Finalize the previous route's metrics before resetting state for the new route.
finalizeCurrentSoftNav();
activeSoftNav = softNavEntry;
// MANDATORY: Seed the LCP candidate with getLargestInteractionContentfulPaint()
// to capture any attributed contentful paints that occurred before the URL updated.
activeLcpEntry = softNavEntry.getLargestInteractionContentfulPaint();
}
});
const icpObserver = new PerformanceObserver((list) => {
for (const icpEntry of list.getEntries()) {
// MANDATORY: Match by interactionId (not navigationId) so only paints caused
// by the soft navigation's initiating interaction update the route's LCP.
if (activeSoftNav && icpEntry.interactionId === activeSoftNav.interactionId) {
activeLcpEntry = icpEntry;
}
}
});
softNavObserver.observe({ type: 'soft-navigation', buffered: true });
icpObserver.observe({ type: 'interaction-contentful-paint', buffered: true });
// Finalize the active route's LCP when the page is backgrounded or unloaded.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
finalizeCurrentSoftNav();
}
});
return { softNavObserver, icpObserver, finalizeCurrentSoftNav };
}
3. 使用 web-vitals 評估軟導覽的 Core Web Vitals
如果您的目標是向數據分析端點回報硬式和軟式導覽的標準 Core Web Vitals (LCP、CLS 和 INP),請使用 web-vitals 程式庫 (v6.0.0+) 搭配 { reportSoftNavs: true },而非手動切割每個指標時間軸。
import { onLCP, onFCP, onCLS, onINP } from 'web-vitals';
function sendToAnalytics(metric) {
// Example-only endpoint; replace with your real telemetry URL.
navigator.sendBeacon(
'/analytics',
JSON.stringify({
name: metric.name,
value: metric.value,
navigationType: metric.navigationType,
navigationURL: metric.navigationURL,
}),
);
}
// Pass { reportSoftNavs: true } to report metrics per soft navigation in addition
// to the initial hard navigation.
onLCP(sendToAnalytics, { reportSoftNavs: true });
onFCP(sendToAnalytics, { reportSoftNavs: true });
onCLS(sendToAnalytics, { reportSoftNavs: true });
onINP(sendToAnalytics, { reportSoftNavs: true });
最佳做法
- 務必在呼叫
observer.observe()之前,使用PerformanceObserver.supportedEntryTypes?.includes(...)偵測'interaction-contentful-paint'和'soft-navigation'功能。 - 務必在觀測
'interaction-contentful-paint'和'soft-navigation'時傳遞{ buffered: true },這樣一來,如果數據分析程式碼以非同步方式初始化,就不會遺漏早期項目。 - 務必在處理新的
soft-navigation項目時呼叫softNavEntry.getLargestInteractionContentfulPaint(),這樣網址更新前發生的繪製作業才會納入 LCP。 - 務必使用
interactionId(而非navigationId) 將interaction-contentful-paint項目與軟導覽比對。 - 建議使用
entry.duration(或entry.presentationTime - entry.startTime) 評估互動繪製延遲時間,而非直接回報entry.presentationTime。所有PerformanceEntry時間戳記都是從初始硬式網頁載入時間起點開始測量,而非從軟式導覽開始。 - 請勿使用
performance.getEntriesByType('soft-navigation')監控長期執行的單頁應用程式,因為瀏覽器的緩衝區僅限前 50 個軟導覽項目。
備用策略
如果 Baseline 目標包含不支援 InteractionContentfulPaint 或 PerformanceSoftNavigation 的瀏覽器,請將這些 API 視為真實使用者監控 (RUM) 的漸進增強功能:
- 功能偵測和優雅降級:使用
PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint')和PerformanceObserver.supportedEntryTypes?.includes('soft-navigation')保護觀察器註冊。在不支援的瀏覽器中,請略過註冊觀察器,讓應用程式執行時不會擲回錯誤。 - 持續監控標準硬導覽:繼續監控所有瀏覽器的標準
'largest-contentful-paint'、'event'和'layout-shift'項目。無論是否支援軟導覽,LargestContentfulPaint都會在使用者首次互動時完成,因此在支援的瀏覽器中評估軟導覽,不會改變或破壞初始硬載入 LCP 評估。