How to Measure LCP with the PerformanceObserver API
PerformanceObserver API for LCP Capture
Largest Contentful Paint (LCP) measures the render timing of the largest visible content element in the viewport. While synthetic tools provide controlled baselines, production tracking requires the PerformanceObserver API to capture real-user timing under actual network and device conditions. Proper observer lifecycle management prevents memory leaks, duplicate callbacks, and metric drift. The API relies on a buffered queue to capture metrics dispatched before your script executes, making cross-browser compatibility checks essential for consistent telemetry. For foundational metric definitions and scoring thresholds, consult Core Web Vitals & Performance Metrics Fundamentals.
// Initialize LCP observer immediately to capture early paint events
const lcpObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
// Process entries in the callback
console.log('LCP entries captured:', entries);
});
lcpObserver.observe({
type: 'largest-contentful-paint',
buffered: true // Critical: captures entries dispatched before script execution
});
Implementation & Configuration Steps
Initialize the observer with { type: 'largest-contentful-paint', buffered: true } to guarantee no pre-load entries are missed. Filter incoming entries by entryType and extract the startTime alongside DOM element references for attribution. Cross-origin image resources must serve proper CORS headers (Access-Control-Allow-Origin), otherwise the browser will mask the element and return 0ms or null candidates. Advanced tuning and candidate prioritization strategies are detailed in LCP Measurement & Optimization.
let lcpValue = 0;
let lcpElement = null;
// Callback handles both buffered and live entries
const lcpCallback = (entryList) => {
const entries = entryList.getEntries();
// The last entry is typically the current LCP candidate
const lastEntry = entries[entries.length - 1];
if (lastEntry.startTime > lcpValue) {
lcpValue = lastEntry.startTime;
lcpElement = lastEntry.element;
// Optional: Log element for debugging (e.g., tag, id, src)
console.log(`New LCP candidate: ${lcpValue.toFixed(2)}ms`, lcpElement);
}
};
// Re-bind observer with the actual callback
const lcpObserverFinal = new PerformanceObserver(lcpCallback);
lcpObserverFinal.observe({ type: 'largest-contentful-paint', buffered: true });
Edge Case Handling & Data Validation
Background tab suspension, dynamic DOM mutations, and SPA route transitions frequently invalidate raw LCP timestamps. Browsers may pause timers when a tab loses focus, artificially inflating metrics. Implement visibilitychange listeners to finalize metrics and discard stale entries. Cross-reference field distributions against lab baselines to isolate network variability or device throttling artifacts. Always track document.visibilityState to ensure metrics are only reported when the page is actively rendered.
let isFinalized = false;
const finalizeLCP = () => {
if (isFinalized || lcpValue === 0) return;
isFinalized = true;
// Prepare payload for transmission
const lcpPayload = {
metric: 'LCP',
value: lcpValue,
element: lcpElement?.tagName || 'unknown',
timestamp: Date.now()
};
transmitToRUM(lcpPayload);
};
// Dispatch on visibility change or page unload
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
finalizeLCP();
}
});
RUM Telemetry & Aggregation Pipeline
Transmit finalized LCP payloads using navigator.sendBeacon() during pagehide or visibilitychange to guarantee delivery without blocking navigation. Server-side aggregation should compute p50, p75, and p95 percentiles across device classes and connection types. Map metric distributions to conversion funnels to prioritize engineering sprints based on actual user impact. Implement payload compression and route telemetry through a dedicated ingestion endpoint to avoid polluting primary analytics streams.
function transmitToRUM(payload) {
const endpoint = '/api/v1/rum/ingest';
const data = JSON.stringify({
...payload,
userAgent: navigator.userAgent,
connection: navigator.connection?.effectiveType || 'unknown',
dpr: window.devicePixelRatio
});
// Beacon guarantees delivery even during page teardown
if (navigator.sendBeacon) {
navigator.sendBeacon(endpoint, new Blob([data], { type: 'application/json' }));
} else {
// Fallback for legacy environments
fetch(endpoint, { method: 'POST', body: data, keepalive: true }).catch(() => {});
}
}
Cross-Metric Integration & Debugging
LCP optimization requires correlating paint timing with main-thread activity. Pair LCP timestamps with interaction latency data to identify long tasks delaying render. Validate early rendering signals via network timing analysis and ensure layout stability to prevent candidate invalidation. Standardize telemetry collection using established implementation patterns for consistent cross-browser reporting.