評估互動和軟導覽繪製時間

標準繪製和回應指標在初始網頁載入後會留下測量差距:

  • 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,請按照下列步驟操作:

  1. 使用 buffered: true 觀察 'soft-navigation' 和 'interaction-contentful-paint' 項目。
  2. 必要:收到 'soft-navigation' 項目時,請呼叫 softNavEntry.getLargestInteractionContentfulPaint(),為初始 LCP 候選項目提供種子。如果應用程式在更新網址「之前」繪製內容 (例如算繪檢視畫面,然後呼叫 history.pushState()),這些早期的 interaction-contentful-paint 項目會在 soft-navigation 項目之前發出,且仍會包含「先前」的 navigationId。
  3. 必要:請比對 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) 的漸進增強功能:

  1. 功能偵測和優雅降級:使用 PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint') 和 PerformanceObserver.supportedEntryTypes?.includes('soft-navigation') 保護觀察器註冊。在不支援的瀏覽器中,請略過註冊觀察器,讓應用程式執行時不會擲回錯誤。
  2. 持續監控標準硬導覽:繼續監控所有瀏覽器的標準 'largest-contentful-paint'、'event' 和 'layout-shift' 項目。無論是否支援軟導覽,LargestContentfulPaint 都會在使用者首次互動時完成,因此在支援的瀏覽器中評估軟導覽,不會改變或破壞初始硬載入 LCP 評估。