Standardowe dane dotyczące renderowania i czasu reakcji pozostawiają lukę pomiarową po początkowym wczytaniu strony:
LargestContentfulPaint(largest-contentful-paint) mierzy tylko początkowe („twarde”) wczytywanie dokumentu i przestaje emitować wpisy, gdy tylko użytkownik po raz pierwszy wejdzie w interakcję ze stroną.PerformanceEventTimingievent/ interakcja do kolejnego wyrenderowania (INP) mierzy czas reakcji na dane wejściowe do następnej klatki (np. stanu aktywności przycisku lub wskaźnika ładowania), ale nie śledzi kolejnych asynchronicznych renderowań, takich jak nowa treść renderowana po rozwiązaniufetch()lub zakończeniu przejścia po stronie klienta.- Obserwowanie zmian ścieżki w aplikacji jednostronicowej może być trudne poza kodem platformy. Samo monitorowanie aktualizacji adresów URL może powodować nadmierne raportowanie zmian adresów URL, które nie są powiązane ze zmianami tras (np. zmiany linków do kotwic, wielokrotne „przekierowania” w przypadku pojedynczej zmiany trasy).
Tę lukę wypełniają typy wpisów dotyczących wydajności InteractionContentfulPaint (interaction-contentful-paint) i PerformanceSoftNavigation (soft-navigation). Przenoszą one przyczynowość interakcji między zadaniami asynchronicznymi (fetch, await, setTimeout), aby przypisywać nowe wyrenderowania treści i przejścia między ścieżkami w aplikacjach jednostronicowych (SPA) bezpośrednio do interakcji użytkownika, która je wywołała.
Jak to zrobić?
1. Obserwowanie renderowania treści wywołanego przez interakcje
Użyj PerformanceObserver z type: 'interaction-contentful-paint' i buffered: true, aby obserwować coraz większe wyrenderowania treści, które można przypisać do dowolnej interakcji użytkownika.
Każdy wpis InteractionContentfulPaint zawiera:
interactionId: unikalny identyfikator interakcji użytkownika, która zainicjowała zdarzenie (odpowiadającyPerformanceEventTiming.interactionIdiPerformanceSoftNavigation.interactionId).startTime: sygnatura czasowa rozpoczęcia interakcji użytkownika, która zainicjowała zdarzenie, względem początkowego czasu nawigacji twardej.paintTimeipresentationTime: kiedy zakończyła się faza renderowania i kiedy piksele zostały narysowane na ekranie.duration: czas, który upłynął od rozpoczęcia interakcji do renderowania (presentationTime - startTime).largestContentfulPaint: obiekt migawkiLargestContentfulPaintzawierający szczegółowe informacje o największym wyrenderowanym elemencie w ramach danej interakcji (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. Pomiar największego wyrenderowania treści (LCP) w przypadku miękkich nawigacji w aplikacjach jednostronicowych
Przeglądarka emituje miękką nawigację, gdy zaufana interakcja użytkownika powoduje zarówno widoczną zmianę adresu URL, jak i wyrenderowanie treści. Aby obliczyć LCP dla każdej nawigacji programowej:
- Obserwuj wpisy
'soft-navigation'i'interaction-contentful-paint'zbuffered: true. - OBOWIĄZKOWE: po otrzymaniu wpisu
'soft-navigation'wywołajsoftNavEntry.getLargestInteractionContentfulPaint(), aby wygenerować początkowy element kandydujący LCP. Jeśli aplikacja wyrenderowała treść przed zaktualizowaniem adresu URL (np. wyrenderowała widok, a następnie wywołałahistory.pushState()), te wcześniejsze wpisyinteraction-contentful-paintzostały wyemitowane przed wpisemsoft-navigationi nadal zawierają poprzedninavigationId. - OBOWIĄZKOWO: powiąż kolejne wpisy
'interaction-contentful-paint'z aktywną nawigacją miękką, dopasowującentry.interactionId === softNavEntry.interactionId, NIE dopasowuj wedługentry.navigationId. UżycieinteractionIdgwarantuje, że uwzględnisz renderowania, które miały miejsce przed aktualizacją adresu URL, i wykluczysz renderowania z późniejszych, niepowiązanych interakcji na nowej trasie.
/**
* 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. Mierzenie Core Web Vitals w przypadku nawigacji miękkiej za pomocą web-vitals
Jeśli Twoim celem jest raportowanie standardowych Core Web Vitals (LCP, CLS i INP) zarówno w przypadku twardych, jak i miękkich nawigacji do punktu końcowego analityki, użyj biblioteki web-vitals (wersja 6.0.0 lub nowsza) z { reportSoftNavs: true } zamiast ręcznego dzielenia każdej osi czasu wskaźników.
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 });
Sprawdzone metody
- DO feature-detect
'interaction-contentful-paint'and'soft-navigation'usingPerformanceObserver.supportedEntryTypes?.includes(...)before callingobserver.observe(). - PRZEKAZUJ
{ buffered: true }podczas obserwowania'interaction-contentful-paint'i'soft-navigation', aby nie przegapić wczesnych wpisów, jeśli kod analityczny inicjuje się asynchronicznie. - WYWOŁUJ funkcję
softNavEntry.getLargestInteractionContentfulPaint()podczas obsługi nowego wpisusoft-navigation, aby wyrenderowania, które miały miejsce przed aktualizacją adresu URL, były uwzględniane w LCP. - DOPASOWUJ wpisy
interaction-contentful-paintdo nawigacji miękkiej za pomocąinteractionId, a nienavigationId. - UŻYWAJ
entry.duration(lubentry.presentationTime - entry.startTime) do pomiaru opóźnienia renderowania interakcji zamiast bezpośredniego raportowaniaentry.presentationTime. Wszystkie sygnatury czasowePerformanceEntrysą mierzone od początkowego czasu wczytywania strony, a nie od rozpoczęcia nawigacji miękkiej. - NIE polegaj na
performance.getEntriesByType('soft-navigation')do monitorowania długotrwałych aplikacji jednostronicowych, ponieważ bufor przeglądarki jest ograniczony do pierwszych 50 wpisów dotyczących łagodnej nawigacji.
Strategie zastępcze
Jeśli Twój cel Baseline obejmuje przeglądarki, które nie obsługują InteractionContentfulPaint ani PerformanceSoftNavigation, traktuj te interfejsy API jako progresywne ulepszanie monitorowania rzeczywistych użytkowników (RUM):
- Wykrywanie funkcji i łagodna degradacja: zabezpiecz rejestrację obserwatora za pomocą
PerformanceObserver.supportedEntryTypes?.includes('interaction-contentful-paint')iPerformanceObserver.supportedEntryTypes?.includes('soft-navigation'). W przeglądarkach, które nie obsługują tej funkcji, pomiń rejestrowanie obserwatorów, aby aplikacja działała bez błędów. - Utrzymuj aktywne standardowe obserwatory twardej nawigacji: nadal obserwuj standardowe wpisy
'largest-contentful-paint','event'i'layout-shift'we wszystkich przeglądarkach. Ponieważ wskaźnikLargestContentfulPaintjest finalizowany przy pierwszej interakcji użytkownika niezależnie od obsługi nawigacji miękkiej, pomiar nawigacji miękkiej w obsługiwanych przeglądarkach nie zmienia ani nie zakłóca początkowych pomiarów LCP przy twardym wczytywaniu.