Las métricas estándar de renderización y capacidad de respuesta dejan una brecha de medición después de la carga inicial de la página:
LargestContentfulPaint(largest-contentful-paint) solo mide la carga inicial ("completa") del documento y deja de emitir entradas en cuanto el usuario interactúa por primera vez con la página.PerformanceEventTimingyevent/ Interaction to Next Paint (INP) miden la capacidad de respuesta de la entrada hasta el siguiente fotograma (como un estado activo de un botón o un ícono giratorio de carga), pero no hacen un seguimiento de las pinturas asíncronas posteriores, como el contenido nuevo que se renderiza después de que se resuelve unfetch()o se completa una transición de ruta del cliente.- Observar los cambios de ruta en una aplicación de una sola página (SPA) puede ser difícil fuera del código del framework. Solo supervisar las actualizaciones de URL puede generar informes excesivos de cambios de URL que no están vinculados a cambios de ruta (por ejemplo, cambios de vínculos de anclaje, varios "redireccionamientos" para un solo cambio de ruta).
Los tipos de entradas de rendimiento InteractionContentfulPaint (interaction-contentful-paint) y PerformanceSoftNavigation (soft-navigation) llenan este vacío. Propagan la causalidad de la interacción en tareas asíncronas (fetch, await y setTimeout) para atribuir nuevos renderizados con contenido y transiciones de ruta de aplicaciones de una sola página (SPA) directamente a la interacción del usuario que los activó.
Cómo implementarlo
1. Observa los procesamientos de imagen con contenido que se activan con las interacciones
Usa un PerformanceObserver con type: 'interaction-contentful-paint' y buffered: true para observar los Largest Contentful Paints progresivamente más grandes que se pueden atribuir a cualquier interacción del usuario.
Cada entrada de InteractionContentfulPaint proporciona lo siguiente:
interactionId: Es el identificador único de la interacción del usuario que inicia la acción (coincide conPerformanceEventTiming.interactionIdyPerformanceSoftNavigation.interactionId).startTime: Es la marca de tiempo en la que comenzó la interacción del usuario que inició la navegación, en relación con el origen de tiempo de la navegación inicial.paintTimeypresentationTime: Cuándo finalizó la fase de renderización y cuándo se dibujaron los píxeles pintados en la pantalla.duration: Es el tiempo transcurrido desde el inicio de la interacción hasta la presentación de la pintura (presentationTime - startTime).largestContentfulPaint: Es un objetoLargestContentfulPaintde instantánea que contiene detalles sobre el elemento renderizado más grande para esa interacción hasta el momento (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. Mide el Largest Contentful Paint (LCP) para las navegaciones suaves de SPA
El navegador emite una navegación suave cuando una interacción de usuario de confianza provoca un cambio de URL visible y un procesamiento con contenido. Para calcular el LCP de cada navegación de una sola página, haz lo siguiente:
- Observa las entradas
'soft-navigation'y'interaction-contentful-paint'conbuffered: true. - OBLIGATORIO: Cuando se reciba una entrada
'soft-navigation', llama asoftNavEntry.getLargestInteractionContentfulPaint()para propagar su candidato inicial de LCP. Si la aplicación pintó contenido antes de actualizar la URL (por ejemplo, renderizando una vista y, luego, llamando ahistory.pushState()), esas entradas tempranas deinteraction-contentful-paintse emitieron antes de la entrada desoft-navigationy aún llevan elnavigationIdanterior. - OBLIGATORIO: Correlaciona las entradas de
'interaction-contentful-paint'posteriores con la navegación suave activa haciendo coincidirentry.interactionId === softNavEntry.interactionId, NO hagas coincidir porentry.navigationId. El uso deinteractionIdgarantiza que incluyas las pinturas que se produjeron antes de la actualización de la URL y excluyas las pinturas de interacciones posteriores no relacionadas en la ruta nueva.
/**
* 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. Mide las Métricas web esenciales en las navegaciones suaves con web-vitals
Si tu objetivo es informar las Core Web Vitals estándar (LCP, CLS e INP) en las navegaciones completas y parciales a un extremo de estadísticas, usa la biblioteca web-vitals (v6.0.0 o posterior) con { reportSoftNavs: true } en lugar de segmentar manualmente cada cronograma de métricas.
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 });
Prácticas recomendadas
- SÍ detecta las funciones
'interaction-contentful-paint'y'soft-navigation'conPerformanceObserver.supportedEntryTypes?.includes(...)antes de llamar aobserver.observe(). - SÍ pasa
{ buffered: true }cuando observes'interaction-contentful-paint'y'soft-navigation'para que no se pierdan las entradas tempranas si el código de estadísticas se inicializa de forma asíncrona. - SÍ llama a
softNavEntry.getLargestInteractionContentfulPaint()cuando se controle una nueva entrada desoft-navigationpara que los procesamientos de imagen que se produjeron antes de la actualización de la URL se incluyan en el LCP. - SÍ debes hacer coincidir las entradas de
interaction-contentful-paintcon una navegación suave usandointeractionId, nonavigationId. - SÍ uses
entry.duration(oentry.presentationTime - entry.startTime) para medir la latencia de renderización de la interacción, en lugar de informarentry.presentationTimedirectamente. Todas las marcas de tiempo dePerformanceEntryse miden desde el origen del tiempo de carga de la página inicial completa, no desde el inicio de la navegación parcial. - NO confíes en
performance.getEntriesByType('soft-navigation')para supervisar aplicaciones de una sola página de larga duración, ya que el búfer del navegador está limitado a las primeras 50 entradas de navegación suave.
Estrategias de resguardo
Si tu objetivo de Baseline incluye navegadores que no admiten InteractionContentfulPaint o PerformanceSoftNavigation, trata estas APIs como una mejora progresiva para la supervisión de usuarios reales (RUM):
- Detección de funciones y degradación elegante: Protege el registro del observador con
PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint')yPerformanceObserver.supportedEntryTypes?.includes('soft-navigation'). En los navegadores que no admiten esta función, omite el registro de los observadores para que tu aplicación se ejecute sin arrojar errores. - Mantén activos los observadores de navegación estándar: Sigue observando las entradas estándar de
'largest-contentful-paint','event'y'layout-shift'en todos los navegadores. Debido a que elLargestContentfulPaintse finaliza en la primera interacción del usuario, independientemente de la compatibilidad con la navegación suave, medir las navegaciones suaves en los navegadores compatibles no altera ni interrumpe las mediciones iniciales del LCP de carga completa.