วัดเวลาในการโต้ตอบและการแสดงผลการนําทางแบบซอฟต์

เมตริกการแสดงผลและการตอบสนองมาตรฐานจะทำให้เกิดช่องว่างในการวัดหลังจากโหลดหน้าเว็บครั้งแรก ดังนี้

  • LargestContentfulPaint (largest-contentful-paint) จะวัดเฉพาะการโหลดเอกสารครั้งแรก ("ฮาร์ด") และหยุดแสดงรายการทันทีที่ผู้ใช้โต้ตอบกับหน้าเว็บเป็นครั้งแรก
  • PerformanceEventTiming และ event / Interaction to Next Paint (INP) วัดการตอบสนองต่ออินพุตจนถึงเฟรมถัดไป (เช่น สถานะการทำงานของปุ่มหรือภาพหมุนขณะโหลด) แต่จะไม่ติดตามการแสดงผลแบบอะซิงโครนัสที่ตามมา เช่น เนื้อหาใหม่ที่แสดงผลหลังจาก fetch() ได้รับการแก้ไข หรือการเปลี่ยนเส้นทางฝั่งไคลเอ็นต์เสร็จสมบูรณ์
  • การสังเกตการเปลี่ยนแปลงเส้นทางในแอปพลิเคชันหน้าเว็บเดียว (SPA) อาจทำได้ยากนอกโค้ดเฟรมเวิร์ก การตรวจสอบการอัปเดต URL เพียงอย่างเดียวอาจรายงานการเปลี่ยนแปลง URL ที่ไม่ได้ลิงก์กับการเปลี่ยนแปลงเส้นทางมากเกินไป (เช่น การเปลี่ยนแปลงลิงก์ตำแหน่งเฉพาะ, "การเปลี่ยนเส้นทาง" หลายรายการสำหรับการเปลี่ยนแปลงเส้นทางเดียว)

ประเภทรายการประสิทธิภาพ InteractionContentfulPaint (interaction-contentful-paint) และ PerformanceSoftNavigation (soft-navigation) จะช่วยเติมเต็มช่องว่างนี้ โดยจะเผยแพร่สาเหตุของการโต้ตอบในงานแบบอะซิงโครนัส (fetch, await, setTimeout) เพื่อระบุแหล่งที่มาของ Contentful Paint ใหม่และการเปลี่ยนเส้นทางแอปพลิเคชันหน้าเว็บเดียว (SPA) กลับไปยังการโต้ตอบของผู้ใช้ที่ทริกเกอร์โดยตรง

วิธีการนำไปใช้งาน

1. สังเกตการแสดงผลเนื้อหาที่ทริกเกอร์โดยการโต้ตอบ

ใช้ PerformanceObserver กับ type: 'interaction-contentful-paint' และ buffered: true เพื่อสังเกตการแสดงเนื้อหาขนาดใหญ่ที่สุดที่เพิ่มขึ้นเรื่อยๆ ซึ่งเกิดจากการโต้ตอบของผู้ใช้

รายการ InteractionContentfulPaint แต่ละรายการจะให้ข้อมูลต่อไปนี้

  • interactionId: ตัวระบุที่ไม่ซ้ำกันของการโต้ตอบของผู้ใช้ที่เริ่มต้น (ตรงกับ 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. วัด Largest Contentful Paint (LCP) สำหรับการนำทางแบบไม่เข้มงวดของ SPA

เบราว์เซอร์จะส่งการนําทางแบบซอฟต์เมื่อการโต้ตอบของผู้ใช้ที่เชื่อถือได้ทําให้เกิดทั้งการเปลี่ยนแปลง URL ที่มองเห็นได้และการแสดงผลเนื้อหา วิธีคำนวณ LCP สำหรับการนำทางแบบซอฟต์แต่ละครั้ง

  1. สังเกตทั้งรายการ 'soft-navigation' และ 'interaction-contentful-paint' ด้วย buffered: true
  2. บังคับ: เมื่อได้รับรายการ 'soft-navigation' ให้สร้างผู้สมัคร LCP เริ่มต้นโดยเรียกใช้ softNavEntry.getLargestInteractionContentfulPaint() หากแอปพลิเคชันแสดงเนื้อหาก่อนอัปเดต URL (เช่น แสดงผลมุมมองแล้วเรียกใช้ history.pushState()) รายการ interaction-contentful-paint ในช่วงแรกๆ เหล่านั้นจะแสดงก่อนรายการ soft-navigation และยังคงมี navigationId ก่อนหน้า
  3. บังคับ: เชื่อมโยงรายการ 'interaction-contentful-paint' ที่ตามมากับการนําทางแบบซอฟต์ที่ใช้งานอยู่โดยจับคู่ entry.interactionId === softNavEntry.interactionId อย่าจับคู่ตาม entry.navigationId การใช้ interactionId ช่วยให้มั่นใจว่าคุณได้รวมการแสดงผลที่เกิดขึ้นก่อนการอัปเดต URL และไม่รวมการแสดงผลจากการโต้ตอบที่ไม่เกี่ยวข้องในภายหลังในเส้นทางใหม่
/**
 * 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. วัด Core Web Vitals ในการนําทางแบบซอฟต์ด้วย 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 });

แนวทางปฏิบัติแนะนำ

  • ควรตรวจหาฟีเจอร์ 'interaction-contentful-paint' และ 'soft-navigation' โดยใช้ PerformanceObserver.supportedEntryTypes?.includes(...) ก่อนเรียกใช้ observer.observe()
  • ควรส่ง { buffered: true } เมื่อสังเกต 'interaction-contentful-paint' และ 'soft-navigation' เพื่อไม่ให้พลาดรายการแรกๆ หากโค้ด Analytics เริ่มต้นแบบอะซิงโครนัส
  • ควรเรียกใช้ softNavEntry.getLargestInteractionContentfulPaint() เมื่อจัดการรายการ soft-navigation ใหม่ เพื่อให้การแสดงผลที่เกิดขึ้นก่อนการอัปเดต URL รวมอยู่ใน LCP
  • ควรจับคู่รายการ interaction-contentful-paint กับการนําทางแบบนุ่มนวลโดยใช้ interactionId ไม่ใช่ navigationId
  • ควรใช้ entry.duration (หรือ entry.presentationTime - entry.startTime) เพื่อวัดเวลาในการตอบสนองของการแสดงผลการโต้ตอบ แทนที่จะรายงาน entry.presentationTime โดยตรง การประทับเวลา PerformanceEntry ทั้งหมดจะวัดจากต้นทางของเวลาในการโหลดหน้าเว็บแบบฮาร์ดเริ่มต้น ไม่ใช่จากการเริ่มต้นการนําทางแบบซอฟต์
  • อย่าใช้ performance.getEntriesByType('soft-navigation') เพื่อตรวจสอบแอปพลิเคชันหน้าเว็บเดียวที่มีอายุการใช้งานยาวนาน เนื่องจากบัฟเฟอร์ของเบราว์เซอร์จำกัดไว้ที่รายการการนําทางแบบซอฟต์ 50 รายการแรก

กลยุทธ์สำรอง

หากเป้าหมาย Baseline ของคุณรวมเบราว์เซอร์ที่ไม่รองรับ InteractionContentfulPaint หรือ PerformanceSoftNavigation ให้ถือว่า API เหล่านี้เป็นการเพิ่มประสิทธิภาพแบบต่อเนื่องสำหรับการตรวจสอบผู้ใช้จริง (RUM) ดังนี้

  1. การตรวจหาฟีเจอร์และการลดลงอย่างค่อยเป็นค่อยไป: ป้องกันการลงทะเบียน Observer ด้วย PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint') และ PerformanceObserver.supportedEntryTypes?.includes('soft-navigation') ในเบราว์เซอร์ที่ไม่มีการรองรับ ให้ข้ามการลงทะเบียน Observer เพื่อให้แอปพลิเคชันทำงานโดยไม่แสดงข้อผิดพลาด
  2. เปิดใช้ Hard Navigation Observer มาตรฐานไว้: สังเกตรายการ 'largest-contentful-paint', 'event' และ 'layout-shift' มาตรฐานต่อไปในเบราว์เซอร์ทั้งหมด เนื่องจาก LargestContentfulPaint จะสรุปผลในการโต้ตอบของผู้ใช้ครั้งแรกไม่ว่าจะรองรับการนําทางแบบซอฟต์หรือไม่ก็ตาม การวัดการนําทางแบบซอฟต์ในเบราว์เซอร์ที่รองรับจึงไม่เปลี่ยนแปลงหรือทําลายการวัด LCP แบบฮาร์ดโหลดเริ่มต้น