As métricas padrão de pintura e responsividade deixam uma lacuna de medição após o carregamento de página inicial:
LargestContentfulPaint(largest-contentful-paint) mede apenas o carregamento inicial ("hard") do documento e para de emitir entradas assim que o usuário interage com a página pela primeira vez.PerformanceEventTimingeevent/ Interaction to Next Paint (INP) medem a capacidade de resposta de entrada até o próximo frame (como um estado ativo de botão ou um ícone de carregamento), mas não rastreiam as pinturas assíncronas subsequentes, como um novo conteúdo renderizado após a resolução de umfetch()ou a conclusão de uma transição de rota do lado do cliente.- Observar mudanças de rota em um aplicativo de página única (SPA) pode ser difícil fora do código do framework. Monitorar apenas as atualizações de URL pode gerar relatórios excessivos de mudanças de URL que não estão vinculadas a mudanças de rota (por exemplo, mudanças de link fixo, vários "redirecionamentos" para uma única mudança de rota).
Os tipos de entrada de performance InteractionContentfulPaint (interaction-contentful-paint) e PerformanceSoftNavigation (soft-navigation) preenchem essa lacuna. Eles propagam a causalidade da interação em tarefas assíncronas (fetch, await, setTimeout) para atribuir novas renderizações de conteúdo e transições de rota de aplicativos de página única (SPA) diretamente à interação do usuário que as acionou.
Como fazer a implementação
1. Observar as Contentful Paints acionadas por interações
Use um PerformanceObserver com type: 'interaction-contentful-paint' e buffered: true para observar as maiores pinturas com conteúdo progressivamente atribuíveis a qualquer interação do usuário.
Cada entrada InteractionContentfulPaint fornece:
interactionId: o identificador exclusivo da interação do usuário que iniciou o evento (correspondente aPerformanceEventTiming.interactionIdePerformanceSoftNavigation.interactionId).startTime: o carimbo de data/hora de quando a interação do usuário que iniciou a navegação começou, em relação à origem do tempo de navegação inicial.paintTimeepresentationTime: quando a fase de renderização terminou e quando os pixels pintados foram desenhados na tela.duration: o tempo decorrido do início da interação até a apresentação da pintura (presentationTime - startTime).largestContentfulPaint: um objeto de snapshotLargestContentfulPaintque contém detalhes sobre o maior elemento pintado para essa interação até o 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. Medir a Largest Contentful Paint (LCP) para navegações suaves de SPA
Uma navegação suave é emitida pelo navegador quando uma interação confiável do usuário causa uma mudança visível no URL e uma exibição de conteúdo. Para calcular a LCP de cada navegação flexível:
- Observe as entradas
'soft-navigation'e'interaction-contentful-paint'combuffered: true. - OBRIGATÓRIO: quando uma entrada
'soft-navigation'for recebida, usesoftNavEntry.getLargestInteractionContentfulPaint()para definir o candidato inicial da LCP. Se o aplicativo tiver pintado o conteúdo antes de atualizar o URL (por exemplo, renderizando uma visualização e chamandohistory.pushState()), as entradasinteraction-contentful-paintiniciais foram emitidas antes da entradasoft-navigatione ainda carregam onavigationIdanterior. - OBRIGATÓRIO: correlacione as entradas
'interaction-contentful-paint'subsequentes à navegação flexível ativa correspondendo aentry.interactionId === softNavEntry.interactionId, NÃO corresponda porentry.navigationId. O uso deinteractionIdgarante que você inclua as pinturas que ocorreram antes da atualização do URL e exclua as pinturas de interações posteriores não relacionadas na nova rota.
/**
* 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. Medir as Core Web Vitals em navegações suaves com web-vitals
Se o objetivo é informar as Core Web Vitals padrão (LCP, CLS e INP) em navegações rígidas e flexíveis para um endpoint de análise, use a biblioteca web-vitals (v6.0.0+) com { reportSoftNavs: true } em vez de dividir manualmente cada linha do tempo de métrica.
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áticas recomendadas
- FAÇA a detecção de recursos de
'interaction-contentful-paint'e'soft-navigation'usandoPerformanceObserver.supportedEntryTypes?.includes(...)antes de chamarobserver.observe(). - RECOMENDAMOS transmitir
{ buffered: true }ao observar'interaction-contentful-paint'e'soft-navigation'para que as entradas iniciais não sejam perdidas se o código de análise for inicializado de forma assíncrona. - CHAME
softNavEntry.getLargestInteractionContentfulPaint()ao processar uma nova entradasoft-navigationpara que as pinturas que ocorreram antes da atualização do URL sejam incluídas na LCP. - FAÇA a correspondência de entradas
interaction-contentful-paintcom uma navegação suave usandointeractionId, nãonavigationId. - USE
entry.duration(ouentry.presentationTime - entry.startTime) para medir a latência de pintura de interação, em vez de informarentry.presentationTimediretamente. Todos os carimbos de data/hora dePerformanceEntrysão medidos a partir da origem do tempo de carregamento da página inicial, não do início da navegação suave. - NÃO use
performance.getEntriesByType('soft-navigation')para monitorar aplicativos de página única de longa duração, já que o buffer do navegador é limitado às primeiras 50 entradas de navegação suave.
Estratégias de fallback
Se a meta do Baseline incluir navegadores que não oferecem suporte a InteractionContentfulPaint ou PerformanceSoftNavigation, trate essas APIs como um aprimoramento progressivo para o monitoramento de usuários reais (RUM, na sigla em inglês):
- Detecção de recursos e degradação suave: proteja o registro do observador com
PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint')ePerformanceObserver.supportedEntryTypes?.includes('soft-navigation'). Em navegadores sem suporte, pule o registro dos observadores para que o aplicativo seja executado sem gerar erros. - Mantenha os observadores de navegação padrão ativos: continue observando as entradas padrão
'largest-contentful-paint','event'e'layout-shift'em todos os navegadores. Como aLargestContentfulPainté finalizada na primeira interação do usuário, independente do suporte à navegação flexível, medir as navegações flexíveis em navegadores compatíveis não altera nem interrompe as medições iniciais de LCP de carregamento rígido.