De timing van interactie en soft-navigatie meten

Standaardstatistieken voor weergave en reactiesnelheid laten een meetgat achter na het eerste laden van de pagina:

  • LargestContentfulPaint (largest-contentful-paint) meet alleen de eerste ('harde') documentlading en stopt met het genereren van vermeldingen zodra de gebruiker voor het eerst interactie heeft met de pagina.
  • PerformanceEventTiming en event / Interaction to Next Paint (INP) meet de responsiviteit van de invoer tot het volgende frame (zoals een actieve knop of een laadspinner), maar houdt geen volgende asynchrone weergaven bij, zoals nieuwe content die wordt gerenderd nadat een fetch() is opgelost of een routetransitie aan de clientzijde is afgerond.
  • Het kan lastig zijn om routewijzigingen in een app met één pagina (Single Page App, SPA) te observeren buiten de frameworkcode. Als je alleen URL-updates bijhoudt, kan dit leiden tot overrapportage van URL-wijzigingen die niet zijn gekoppeld aan routewijzigingen (bijvoorbeeld wijzigingen in ankerlinks, meerdere omleidingen voor één routewijziging).

De prestatie-invoertypen InteractionContentfulPaint (interaction-contentful-paint) en PerformanceSoftNavigation (soft-navigation) vullen deze leemte op. Ze verspreiden de causaliteit van interacties over asynchrone taken (fetch, await, setTimeout) om nieuwe weergaven met content en routetransities van apps met één pagina (SPA) rechtstreeks toe te schrijven aan de gebruikersinteractie die ze heeft geactiveerd.

Hoe wordt dit geïmplementeerd

1. Observeer contentful paints die worden geactiveerd door interacties

Gebruik een PerformanceObserver met type: 'interaction-contentful-paint' en buffered: true om steeds grotere contentful paints te observeren die kunnen worden toegeschreven aan een gebruikersinteractie.

Elke InteractionContentfulPaint-vermelding bevat:

  • interactionId: De unieke ID van de initiërende gebruikersinteractie (overeenkomend met PerformanceEventTiming.interactionId en PerformanceSoftNavigation.interactionId).
  • startTime: De tijdstempel van wanneer de initiërende gebruikersinteractie is gestart, relatief ten opzichte van de oorspronkelijke tijd van de harde navigatie.
  • paintTime en presentationTime: Wanneer de weergavefase is beëindigd en wanneer de getekende pixels op het scherm zijn getekend.
  • duration: De verstreken tijd vanaf het begin van de interactie tot de weergave van de tekening (presentationTime - startTime).
  • largestContentfulPaint: Een momentopname LargestContentfulPaint-object met details over het grootste getekende element voor die interactie tot nu toe (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) meten voor soft navigaties van SPA's

Een zachte navigatie wordt door de browser uitgevoerd als een vertrouwde gebruikersinteractie zowel een zichtbare URL-wijziging als een contentful paint veroorzaakt. Zo bereken je de LCP voor elke soft navigation:

  1. Observeer zowel 'soft-navigation'- als 'interaction-contentful-paint'-items met buffered: true.
  2. VERPLICHT: Als er een 'soft-navigation'-item wordt ontvangen, seed je de eerste LCP-kandidaat door softNavEntry.getLargestInteractionContentfulPaint() aan te roepen. Als de app content heeft getekend voordat de URL werd geüpdatet (bijvoorbeeld door een weergave te renderen en daarna history.pushState() aan te roepen), zijn die vroege interaction-contentful-paint-items uitgevoerd vóór het soft-navigation-item en bevatten ze nog steeds de vorige navigationId.
  3. VERPLICHT: Koppel daaropvolgende 'interaction-contentful-paint'-items aan de actieve soft-navigatie door entry.interactionId === softNavEntry.interactionId te matchen. Match NIET op entry.navigationId. Als je interactionId gebruikt, zorg je dat je weergaven opneemt die plaatsvonden vóór de URL-update en weergaven uitsluit van latere, niet-gerelateerde interacties op de nieuwe route.
/**
 * 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. Meet Core Web Vitals voor zachte navigaties met web-vitals

Als je de standaard Core Web Vitals (LCP, CLS en INP) voor zowel harde als zachte navigaties wilt rapporteren aan een analyse-eindpunt, gebruik je de web-vitals-bibliotheek (v6.0.0+) met { reportSoftNavs: true } in plaats van elke tijdlijn van statistieken handmatig te segmenteren.

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

Praktische tips

  • WEL functiedetectie 'interaction-contentful-paint' en 'soft-navigation' met PerformanceObserver.supportedEntryTypes?.includes(...) voordat je observer.observe() aanroept.
  • Geef { buffered: true } WEL door als u 'interaction-contentful-paint' en 'soft-navigation' waarneemt, zodat vroege vermeldingen niet worden gemist als de analysecode asynchroon wordt geïnitialiseerd.
  • Roep softNavEntry.getLargestInteractionContentfulPaint() WEL aan als je een nieuwe soft-navigation-vermelding verwerkt, zodat weergaven die vóór de URL-update hebben plaatsgevonden, worden opgenomen in LCP.
  • Zorg dat interaction-contentful-paint-items overeenkomen met een geleidelijke navigatie met interactionId, niet met navigationId.
  • Gebruik WEL entry.duration (of entry.presentationTime - entry.startTime) om de latentie van de interactieweergave te meten in plaats van entry.presentationTime rechtstreeks te rapporteren. Alle PerformanceEntry-tijdstempels worden gemeten vanaf de oorspronkelijke laadtijd van pagina, niet vanaf de geleidelijke start van de navigatie.
  • Vertrouw NIET op performance.getEntriesByType('soft-navigation') om langdurige apps met één pagina te controleren, omdat de buffer van de browser beperkt is tot de eerste 50 vermeldingen voor zachte navigatie.

Reservestrategieën

Als je Baseline-doel browsers bevat die InteractionContentfulPaint of PerformanceSoftNavigation niet ondersteunen, behandel je deze API's als een progressive enhancement voor Real User Monitoring (RUM):

  1. Functiedetectie en graceful degradation: Beveilig de registratie van de observer met PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint') en PerformanceObserver.supportedEntryTypes?.includes('soft-navigation'). In browsers die geen ondersteuning bieden, slaat u de registratie van de observers over, zodat uw app wordt uitgevoerd zonder fouten te genereren.
  2. Houd standaard harde navigatie-observers actief: Blijf standaard 'largest-contentful-paint'-, 'event'- en 'layout-shift'-items observeren in alle browsers. Omdat LargestContentfulPaint wordt afgerond bij de eerste gebruikersinteractie, ongeacht de ondersteuning voor zachte navigatie, worden de eerste LCP-metingen voor hard laden niet gewijzigd of onderbroken door zachte navigatie te meten in ondersteunende browsers.