Les métriques standards de peinture et de réactivité laissent un écart de mesure après le chargement initial de la page :
LargestContentfulPaint(largest-contentful-paint) ne mesure que le chargement initial ("hard") du document et cesse d'émettre des entrées dès que l'utilisateur interagit pour la première fois avec la page.PerformanceEventTimingetevent/Interaction to Next Paint (INP) mesurent la réactivité des entrées jusqu'au frame suivant (comme un état actif de bouton ou un indicateur de chargement), mais ne suivent pas les peintures asynchrones ultérieures, telles que le nouveau contenu affiché après la résolution d'unfetch()ou la fin d'une transition de routage côté client.- Il peut être difficile d'observer les changements de route dans une application monopage (SPA) en dehors du code du framework. Le simple fait de surveiller les mises à jour d'URL peut entraîner une surestimation des modifications d'URL qui ne sont pas liées à des changements de route (par exemple, les modifications de liens d'ancrage ou les "redirections" multiples pour un seul changement de route).
Les types d'entrées de performances InteractionContentfulPaint (interaction-contentful-paint) et PerformanceSoftNavigation (soft-navigation) comblent cette lacune. Ils propagent la causalité des interactions entre les tâches asynchrones (fetch, await, setTimeout) pour attribuer les nouveaux affichages de contenu et les transitions de routage des applications monopages (SPA) directement à l'interaction utilisateur qui les a déclenchés.
Mise en œuvre
1. Observer les Contentful Paints déclenchés par les interactions
Utilisez un PerformanceObserver avec type: 'interaction-contentful-paint' et buffered: true pour observer les Largest Contentful Paint progressivement plus grands attribuables à toute interaction utilisateur.
Chaque entrée InteractionContentfulPaint fournit les informations suivantes :
interactionId: identifiant unique de l'interaction utilisateur à l'origine de l'événement (correspondant àPerformanceEventTiming.interactionIdetPerformanceSoftNavigation.interactionId).startTime: code temporel du début de l'interaction utilisateur à l'origine de la navigation, par rapport à l'origine temporelle de la navigation initiale.paintTimeetpresentationTime: fin de la phase d'affichage et dessin des pixels peints à l'écran.duration: temps écoulé entre le début de l'interaction et la présentation de l'affichage (presentationTime - startTime).largestContentfulPaint: objetLargestContentfulPaintd'instantané contenant des détails sur le plus grand élément peint pour cette interaction jusqu'à présent (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. Mesurer le Largest Contentful Paint (LCP) pour les navigations douces dans les SPA
Une navigation logicielle est émise par le navigateur lorsqu'une interaction utilisateur de confiance entraîne à la fois un changement d'URL visible et un affichage de contenu. Pour calculer le LCP pour chaque navigation réactive :
- Observez les entrées
'soft-navigation'et'interaction-contentful-paint'avecbuffered: true. - OBLIGATOIRE : lorsqu'une entrée
'soft-navigation'est reçue, amorcez son candidat LCP initial en appelantsoftNavEntry.getLargestInteractionContentfulPaint(). Si l'application a peint du contenu avant de mettre à jour l'URL (par exemple, en affichant une vue, puis en appelanthistory.pushState()), ces premières entréesinteraction-contentful-paintont été émises avant l'entréesoft-navigationet portent toujours l'navigationIdprécédent. - OBLIGATOIRE : Corrélez les entrées
'interaction-contentful-paint'suivantes à la navigation logicielle active en faisant correspondreentry.interactionId === softNavEntry.interactionId, NE FAITES PAS correspondre parentry.navigationId. L'utilisation deinteractionIdvous permet d'inclure les peintures qui ont eu lieu avant la mise à jour de l'URL et d'exclure les peintures des interactions ultérieures non liées sur la nouvelle 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. Mesurer les métriques Core Web Vitals pour les navigations douces avec web-vitals
Si votre objectif est de signaler les métriques Core Web Vitals standards (LCP, CLS et INP) pour les navigations complètes et partielles à un point de terminaison d'analyse, utilisez la bibliothèque web-vitals (v6.0.0 ou ultérieure) avec { reportSoftNavs: true } plutôt que de segmenter manuellement chaque chronologie de métrique.
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 });
Bonnes pratiques
- À FAIRE : Détectez les fonctionnalités
'interaction-contentful-paint'et'soft-navigation'à l'aide dePerformanceObserver.supportedEntryTypes?.includes(...)avant d'appelerobserver.observe(). - À FAIRE : Transmettez
{ buffered: true }lorsque vous observez'interaction-contentful-paint'et'soft-navigation'afin de ne pas manquer les entrées précoces si le code d'analyse s'initialise de manière asynchrone. - À FAIRE : Appelez
softNavEntry.getLargestInteractionContentfulPaint()lors du traitement d'une nouvelle entréesoft-navigationafin que les peintures effectuées avant la mise à jour de l'URL soient incluses dans le LCP. - FAITES correspondre les entrées
interaction-contentful-paintà une navigation douce à l'aide deinteractionId, et non denavigationId. - À FAIRE : Utilisez
entry.duration(ouentry.presentationTime - entry.startTime) pour mesurer la latence d'affichage de l'interaction, plutôt que de signaler directemententry.presentationTime. Tous les codes temporelsPerformanceEntrysont mesurés à partir de l'origine du temps de chargement initial de la page, et non à partir du début de la navigation logicielle. - NE VOUS FIEZ PAS à
performance.getEntriesByType('soft-navigation')pour surveiller les applications monopages de longue durée, car le tampon du navigateur est limité aux 50 premières entrées de navigation logicielle.
Stratégies de remplacement
Si votre cible Baseline inclut des navigateurs qui ne sont pas compatibles avec InteractionContentfulPaint ou PerformanceSoftNavigation, traitez ces API comme une amélioration progressive pour le Real User Monitoring (RUM) :
- Détection des fonctionnalités et dégradation élégante : protégez l'enregistrement de l'observateur avec
PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint')etPerformanceObserver.supportedEntryTypes?.includes('soft-navigation'). Dans les navigateurs non compatibles, ignorez l'enregistrement des observateurs pour que votre application s'exécute sans générer d'erreurs. - Maintenez les observateurs de navigation standard actifs : continuez à observer les entrées
'largest-contentful-paint','event'et'layout-shift'standards dans tous les navigateurs. Étant donné queLargestContentfulPaintest finalisé lors de la première interaction de l'utilisateur, que la navigation logicielle soit prise en charge ou non, la mesure des navigations logicielles dans les navigateurs compatibles ne modifie ni n'interrompt les mesures initiales du LCP lors du chargement matériel.