Core Web Vitals

Performance and Core Web Vitals

DevTools and Lighthouse 30,824 (Network and Lighthouse) give you lab data from one synthetic visit. Real users bring slow phones and flaky networks, so you also need field data: timings collected in their browsers and sent to your analytics (real-user monitoring). Every timing API writes PerformanceEntry objects into one timeline, read with performance.getEntriesByType() or a PerformanceObserver (PerformanceObserver).

Performance timeline entry types
Entry types (API) Measures Support
mark, measure (User Timing) Your own named spans Widely available
navigation, resource DNS, TLS, TTFB, load phases Widely available
paint and Server-Timing FCP; server-sent durations Widely available
largest-contentful-paint Largest element painted Newly (Dec 2025)
event, first-input Input latency per event Newly (Dec 2025)
layout-shift, long-animation-frame Shifts; slow frames (Long Tasks) Chromium 4,389 only
Core Web Vitals

Google condenses page experience into three Core Web Vitals, judged at the 75th percentile of page loads (thresholds in Network and Lighthouse). LCP is the render time of the largest image or text block, updated as larger elements paint until the user taps, scrolls or types; Chromium skips invisible elements, full-viewport backgrounds and low-entropy placeholder images. INP, which replaced First Input Delay on March 12, 2024, is the slowest interaction from input to next frame, ignoring the single worst one per 50 interactions. CLS adds up layout-shift scores (impact fraction times distance fraction) in bursts of shifts less than 1 s apart and at most 5 s long, and reports the worst burst.

Getting the edge cases right (hidden tabs, bfcache restores, events belonging to one interaction) is fiddly, so use Google's web-vitals 8,626 library (github.com/GoogleChrome/web-vitals (https://github.com/GoogleChrome/web-vitals 8,626 ), Apache 2.0, about 3 KB brotli, npm 2,036 install web-vitals). Version 6.2.2 exports onLCP, onINP, onCLS, onFCP and onTTFB; version 6 added reportSoftNavs: true for single-page-app route changes in Chromium 151+. Below, the test server delays hero.svg by 800 ms, an ad lands after 1 s without reserved space, and the click handler blocks for 250 ms.

Provoking and measuring LCP, CLS and INPHTMLLive
<!doctype html>
<h1>Product page</h1>
<p id="promo"></p>
<img src="/hero.svg" width="1000" height="400" alt="Hero">
<button id="buy">Add to cart</button>
<script type="module">
  import { onTTFB, onLCP, onINP, onCLS }
    from 'https://cdn.jsdelivr.net/npm/web-vitals@6.2.2/dist/web-vitals.js';
  const log = ({ name, value, rating }) => console.log(name, +value.toFixed(3), rating);
  for (const on of [onTTFB, onLCP, onINP, onCLS]) on(log, { reportAllChanges: true });
  setTimeout(() => {                      // late ad with no reserved space
    promo.innerHTML = '<img src="/ad.svg" width="1000" height="250" alt="Ad">';
  }, 1000);
  buy.addEventListener('click', () => {
    performance.mark('cart');             // User Timing
    const end = performance.now() + 250;
    while (performance.now() < end);      // 250 ms of blocking work
    console.log('measure', Math.round(performance.measure('add', 'cart').duration));
  });
</script>

Headless Chrome 152 1 (1280x720, scripted click at 1.5 s) reports LCP for the heading, then the hero. Callbacks receive { name, value, rating, delta, id, entries }. In production drop reportAllChanges and send each metric with navigator.sendBeacon() (Beacon); import web-vitals/attribution to learn the culprit: the LCP element, the most-shifted node, or the INP target with its long-animation-frame scripts.

To learn which of your functions are slow on real devices, the JS Self-Profiling API samples the stack from inside the page: serve the document with Document-Policy: js-profiling, then new Profiler({ sampleInterval: 10, maxBufferSize: 10_000 }) records and await profiler.stop() resolves to a trace of frames, stacks and samples to ship with the metric. Chromium 94+ only, and sampling costs time, so profile a fraction of visits.

The back/forward cache and Speculation Rules

The bfcache freezes a whole page in memory when you leave, so Back restores it instantly. All major browsers have one (Chrome since 96). A restore fires pageshow with persisted === true instead of load; notRestoredReasons (Chromium 125+) explains failures.

Detecting bfcache restores and blocking reasonsHTMLLive
<a href="other.html">Next page</a>
<script>
  addEventListener('pageshow', (event) => {
    const [nav] = performance.getEntriesByType('navigation');
    console.log(`pageshow persisted=${event.persisted}, navigation type=${nav.type}`);
    const reasons = nav.notRestoredReasons?.reasons ?? [];
    if (reasons.length) console.log('not restored:', reasons.map((r) => r.reason).join());
  });
  addEventListener('pagehide', (event) => console.log(`pagehide persisted=${event.persisted}`));
  if (location.search === '?block') addEventListener('unload', () => {});
</script>

The last three lines are the same visit with ?block. Chrome 152 kept pages with an open IndexedDB connection, a BroadcastChannel or a held Web Lock, but one empty unload listener evicted it. masked means the browser withheld the reason; others include unload-listener, response-cache-control-no-store and websocket. Use pagehide instead of unload, and check Application > Back/forward cache in DevTools.

Speculation Rules (Performance Hints in Markup) let Chromium prerender the next page. Until activation, document.prerendering is true; afterwards the navigation entry's activationStart (Chromium 108+) marks when the user saw it. web-vitals subtracts it from LCP and TTFB, so prerendered visits report near-zero LCP; delay analytics until the prerenderingchange event.