Đo lường thời gian hiển thị nội dung điều hướng mềm và tương tác

Các chỉ số về khả năng phản hồi và hiển thị tiêu chuẩn sẽ để lại một khoảng trống đo lường sau lần tải trang đầu tiên:

  • LargestContentfulPaint (largest-contentful-paint) chỉ đo lường lượt tải tài liệu ban đầu ("hard") và ngừng phát ra các mục ngay khi người dùng tương tác lần đầu với trang.
  • PerformanceEventTiming và event / Lượt tương tác đến nội dung hiển thị tiếp theo (INP) đo lường khả năng phản hồi của hoạt động đầu vào cho đến khung hình tiếp theo (chẳng hạn như trạng thái hoạt động của nút hoặc vòng quay tải), nhưng không theo dõi các lượt hiển thị không đồng bộ tiếp theo, chẳng hạn như nội dung mới được hiển thị sau khi fetch() phân giải hoặc quá trình chuyển đổi tuyến phía máy khách hoàn tất.
  • Việc quan sát các thay đổi về tuyến đường trong Ứng dụng trang đơn (SPA) có thể gặp khó khăn bên ngoài mã khung. Chỉ theo dõi nội dung cập nhật URL có thể báo cáo quá mức các thay đổi về URL không liên kết với thay đổi về tuyến (ví dụ: thay đổi về đường dẫn liên kết neo, nhiều "lượt chuyển hướng" cho một thay đổi về tuyến).

Các loại mục nhập hiệu suất InteractionContentfulPaint (interaction-contentful-paint) và PerformanceSoftNavigation (soft-navigation) sẽ lấp đầy khoảng trống này. Các API này truyền tải mối quan hệ nhân quả của hoạt động tương tác trên các tác vụ không đồng bộ (fetch, await, setTimeout) để phân bổ trực tiếp các lượt hiển thị nội dung mới và quá trình chuyển đổi tuyến đường của Ứng dụng một trang (SPA) cho hoạt động tương tác của người dùng đã kích hoạt các lượt hiển thị và quá trình chuyển đổi đó.

Cách triển khai

1. Quan sát nội dung hiển thị được kích hoạt bằng các lượt tương tác

Sử dụng PerformanceObserver với type: 'interaction-contentful-paint' và buffered: true để quan sát nội dung lớn nhất hiển thị dần dần do bất kỳ hoạt động tương tác nào của người dùng.

Mỗi mục InteractionContentfulPaint cung cấp:

  • interactionId: Giá trị nhận dạng riêng biệt của hoạt động tương tác bắt đầu của người dùng (khớp với PerformanceEventTiming.interactionId và PerformanceSoftNavigation.interactionId).
  • startTime: Dấu thời gian khi hoạt động tương tác của người dùng bắt đầu, so với gốc thời gian điều hướng cứng ban đầu.
  • paintTime và presentationTime: Thời điểm giai đoạn kết xuất kết thúc và thời điểm các pixel được vẽ trên màn hình.
  • duration: Thời gian đã trôi qua từ khi bắt đầu tương tác đến khi hiển thị nội dung (presentationTime - startTime).
  • largestContentfulPaint: Đối tượng ảnh chụp nhanh LargestContentfulPaint chứa thông tin chi tiết về phần tử được vẽ lớn nhất cho hoạt động tương tác đó cho đến nay (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. Đo lường Thời gian hiển thị nội dung lớn nhất (LCP) cho các thao tác điều hướng mềm của SPA

Trình duyệt sẽ phát ra một lượt điều hướng mềm khi một hoạt động tương tác đáng tin cậy của người dùng khiến cả URL hiển thị và nội dung hiển thị đều thay đổi. Cách tính LCP cho mỗi lần điều hướng mềm:

  1. Quan sát cả mục nhập 'soft-navigation' và 'interaction-contentful-paint' bằng buffered: true.
  2. BẮT BUỘC: Khi nhận được một mục nhập 'soft-navigation', hãy gieo ứng viên LCP ban đầu bằng cách gọi softNavEntry.getLargestInteractionContentfulPaint(). Nếu ứng dụng vẽ nội dung trước khi cập nhật URL (ví dụ: kết xuất một khung hiển thị rồi gọi history.pushState()), thì các mục nhập interaction-contentful-paint ban đầu đó sẽ được phát ra trước mục nhập soft-navigation và vẫn mang navigationId trước đó.
  3. BẮT BUỘC: Liên kết các mục nhập 'interaction-contentful-paint' tiếp theo với thao tác điều hướng mềm đang hoạt động bằng cách so khớp entry.interactionId === softNavEntry.interactionId, KHÔNG so khớp theo entry.navigationId. Việc sử dụng interactionId đảm bảo bạn bao gồm các lượt vẽ xảy ra trước khi cập nhật URL và loại trừ các lượt vẽ khỏi các lượt tương tác sau này, không liên quan trên tuyến mới.
/**
 * 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. Đo lường Các chỉ số quan trọng về trang web trong các thao tác điều hướng mềm bằng web-vitals

Nếu mục tiêu của bạn là báo cáo các chỉ số Core Web Vitals tiêu chuẩn (LCP, CLS và INP) trên cả thao tác điều hướng cứng và mềm đến một điểm cuối phân tích, hãy sử dụng thư viện web-vitals (phiên bản 6.0.0 trở lên) với { reportSoftNavs: true } thay vì phân chia thủ công từng dòng thời gian của chỉ số.

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 });

Các phương pháp hay nhất

  • NÊN phát hiện tính năng 'interaction-contentful-paint' và 'soft-navigation' bằng PerformanceObserver.supportedEntryTypes?.includes(...) trước khi gọi observer.observe().
  • NÊN truyền { buffered: true } khi quan sát 'interaction-contentful-paint' và 'soft-navigation' để không bỏ lỡ các mục nhập sớm nếu mã phân tích khởi chạy không đồng bộ.
  • NÊN gọi softNavEntry.getLargestInteractionContentfulPaint() khi xử lý một mục nhập soft-navigation mới để các lần hiển thị xảy ra trước khi cập nhật URL được đưa vào LCP.
  • NÊN so khớp các mục nhập interaction-contentful-paint với một thao tác điều hướng mềm bằng interactionId, chứ không phải navigationId.
  • NÊN sử dụng entry.duration (hoặc entry.presentationTime - entry.startTime) để đo lường độ trễ hiển thị tương tác, thay vì báo cáo trực tiếp entry.presentationTime. Tất cả dấu thời gian PerformanceEntry đều được đo lường từ điểm gốc thời gian tải trang cứng ban đầu, chứ không phải từ thời điểm bắt đầu điều hướng mềm.
  • KHÔNG dựa vào performance.getEntriesByType('soft-navigation') để theo dõi các ứng dụng một trang tồn tại lâu dài, vì vùng đệm của trình duyệt chỉ giới hạn ở 50 mục điều hướng mềm đầu tiên.

Chiến lược dự phòng

Nếu mục tiêu Baseline của bạn bao gồm các trình duyệt không hỗ trợ InteractionContentfulPaint hoặc PerformanceSoftNavigation, hãy coi các API này là một cải tiến tăng dần cho tính năng Giám sát người dùng thực (RUM):

  1. Phát hiện tính năng và xuống cấp nhẹ: Bảo vệ việc đăng ký trình quan sát bằng PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint') và PerformanceObserver.supportedEntryTypes?.includes('soft-navigation'). Trong các trình duyệt không hỗ trợ, hãy bỏ qua việc đăng ký trình quan sát để ứng dụng của bạn chạy mà không gặp lỗi.
  2. Duy trì hoạt động của trình quan sát điều hướng cứng tiêu chuẩn: Tiếp tục quan sát các mục nhập 'largest-contentful-paint', 'event' và 'layout-shift' tiêu chuẩn trên tất cả trình duyệt. Vì LargestContentfulPaint được hoàn tất trong lần tương tác đầu tiên của người dùng bất kể có hỗ trợ thao tác điều hướng mềm hay không, nên việc đo lường thao tác điều hướng mềm trong các trình duyệt hỗ trợ sẽ không làm thay đổi hoặc làm hỏng các phép đo LCP tải cứng ban đầu.