Search…

Performance fundamentals: Core Web Vitals

In this series (12 parts)
  1. What frontend system design covers
  2. Rendering strategies: CSR, SSR, SSG, ISR
  3. Performance fundamentals: Core Web Vitals
  4. Loading performance and resource optimization
  5. State management at scale
  6. Component architecture and design systems
  7. Client-side caching and offline support
  8. Real-time on the frontend
  9. Frontend security
  10. Scalability for frontend systems
  11. Accessibility as a system design concern
  12. Monitoring and observability for frontends

Performance is not a feeling. It is a set of measurable values that correlate directly with user satisfaction, engagement, and revenue. Core Web Vitals give us three specific metrics to target, each representing a different dimension of user experience. If you chose a rendering strategy in the previous article, these metrics tell you whether that choice is actually working.

The three metrics

Google defines three Core Web Vitals that together capture loading speed, interactivity, and visual stability:

  • LCP (Largest Contentful Paint): How quickly does the main content appear?
  • INP (Interaction to Next Paint): How quickly does the page respond to user input?
  • CLS (Cumulative Layout Shift): Does the page jump around while loading?

Each metric has a “good,” “needs improvement,” and “poor” threshold.

The thresholds above are measured at the 75th percentile of page loads. This means 75% of your users must experience values at or below the “good” threshold for your site to pass.

Largest Contentful Paint (LCP)

LCP measures when the largest visible content element finishes rendering. This is typically a hero image, a heading, or a large block of text. It answers the user’s core question: “Has this page loaded?”

What counts as the LCP element

The browser considers these element types:

  • <img> elements
  • <image> inside SVG
  • <video> poster images
  • Elements with background images loaded via url()
  • Block-level elements containing text nodes

The LCP element can change during page load. If a heading renders first and an image renders later (and is larger), the image becomes the new LCP element.

Common LCP problems

graph TD
  A[Slow LCP] --> B[Slow server response]
  A --> C[Render-blocking resources]
  A --> D[Slow resource load time]
  A --> E[Client-side rendering]

  B --> B1[Optimize TTFB with caching/CDN]
  C --> C1[Defer non-critical CSS/JS]
  D --> D1[Optimize images, use modern formats]
  E --> E1[Use SSR or SSG for critical content]

Common causes of slow LCP and their solutions. Most LCP issues trace back to one of these four root causes.

The four main causes of poor LCP are:

  1. Slow server response time. If the server takes 2 seconds to respond, LCP cannot possibly be under 2.5 seconds. Use caching, CDNs, and efficient server-side rendering.
  2. Render-blocking resources. Large CSS files and synchronous JavaScript in the <head> block rendering. Inline critical CSS and defer the rest.
  3. Slow resource load times. Hero images that are 2MB PNGs instead of 100KB WebP. Optimize images aggressively.
  4. Client-side rendering. If the LCP element depends on JavaScript execution and API calls, you have added several seconds to LCP by design.

Optimizing LCP

Each optimization builds on the previous. The largest single improvement typically comes from fixing the server response time or switching rendering strategies. Image optimization and resource hints provide the next tier of gains.

Interaction to Next Paint (INP)

INP replaced First Input Delay (FID) in March 2024. While FID only measured the delay of the first interaction, INP measures the responsiveness of all interactions throughout the page’s lifecycle. It captures the worst-case interaction latency (specifically, the 98th percentile).

An interaction is a click, tap, or key press. INP measures the total time from the user’s input to the next frame painted on screen.

The three phases of an interaction

Every interaction goes through three phases:

  1. Input delay. Time between the user’s action and the event handler starting. This happens when the main thread is busy with other work.
  2. Processing time. Time spent in event handlers.
  3. Presentation delay. Time for the browser to calculate layout, paint, and composite the next frame.
sequenceDiagram
  participant User
  participant Main Thread
  participant Browser

  User->>Main Thread: Click event
  Note over Main Thread: Input Delay (main thread busy)
  Main Thread->>Main Thread: Run event handler(s)
  Note over Main Thread: Processing Time
  Main Thread->>Browser: Commit layout/paint
  Note over Browser: Presentation Delay
  Browser->>User: Next frame painted
  Note over User: Total = INP

The three phases of an interaction that make up INP. Reducing any phase improves the total.

Common INP problems

Poor INP almost always comes from one source: the main thread is busy. JavaScript is single-threaded. If a long-running task occupies the thread when the user clicks, the click waits.

Typical culprits:

  • Large component re-renders. A state change triggers a cascade of re-renders across hundreds of components.
  • Expensive computations on the main thread. Filtering a 10,000-item list synchronously blocks input.
  • Third-party scripts. Analytics, ads, and chat widgets compete for the same thread.
  • Layout thrashing. Reading and writing DOM properties in a loop forces synchronous layout calculations.

Optimizing INP

Break long tasks into smaller chunks using requestIdleCallback, setTimeout(fn, 0), or the scheduler.yield() API. Move expensive work to Web Workers. Use React.memo, useMemo, and virtualization to reduce re-render scope.

A practical rule: no single task should block the main thread for more than 50ms. This is Google’s definition of a “long task” and is the primary target for INP optimization.

Cumulative Layout Shift (CLS)

CLS measures visual stability. Every time a visible element moves unexpectedly, the browser calculates a layout shift score based on how much of the viewport shifted and how far it moved.

What causes layout shifts

Layout shifts happen when the browser does not know the size of an element before it loads:

  • Images without dimensions. The browser cannot reserve space until the image loads.
  • Ads and embeds. Third-party content injected into the page pushes existing content down.
  • Dynamically injected content. A banner or notification appears above existing content.
  • Web fonts. Font swap causes text to reflow when the custom font loads and has different metrics than the fallback.

Measuring CLS correctly

CLS uses session windows. A session window is a group of layout shifts that occur within 1 second of each other, with a maximum window duration of 5 seconds. The CLS score is the maximum session window score, not the sum of all shifts.

This means a single large shift is worse than many small shifts spread over time. It also means that user-initiated shifts (scrolling, tapping an accordion) do not count; only unexpected shifts are measured.

Fixing CLS

The most impactful fixes:

  1. Always set width and height on images and videos. The browser uses these to calculate the aspect ratio and reserve space.
  2. Use aspect-ratio CSS for responsive media containers.
  3. Reserve space for ads and embeds with fixed-height containers.
  4. Use font-display: optional or size-adjusted fallback fonts to eliminate font swap shifts.

Lab vs field data

A critical distinction in performance measurement is lab data vs field data.

Lab data comes from tools like Lighthouse, WebPageTest, and Chrome DevTools. You control the network, device, and conditions. Lab data is reproducible and useful for debugging.

Field data (also called Real User Monitoring or RUM) comes from actual users on real devices and networks. Chrome User Experience Report (CrUX) aggregates field data across millions of Chrome users.

graph LR
  subgraph Lab Data
      A[Lighthouse] --> D[Controlled conditions]
      B[WebPageTest] --> D
      C[Chrome DevTools] --> D
      D --> E[Reproducible, debuggable]
  end

  subgraph Field Data
      F[CrUX] --> I[Real user conditions]
      G[RUM tools] --> I
      H[Analytics] --> I
      I --> J[Reflects actual experience]
  end

  E -.- K[Use for diagnosis]
  J -.- L[Use for monitoring]

Lab data helps you find problems. Field data tells you if real users actually experience them.

The gap between lab and field data can be enormous. A page that scores 95 in Lighthouse might have poor Core Web Vitals in the field because your users are on slower devices or networks than your test environment simulates.

Always prioritize field data for making architectural decisions. Use lab data for diagnosing specific issues after field data reveals a problem.

Performance budgets

A performance budget sets concrete limits on metrics that the team agrees not to exceed. Without a budget, performance degrades gradually as features accumulate. Nobody notices a 50ms regression, but fifty of them add up to 2.5 seconds.

Effective budgets include:

  • LCP under 2.0 seconds at the 75th percentile (leaves headroom below the 2.5s threshold).
  • INP under 150ms at the 75th percentile.
  • CLS under 0.05 at the 75th percentile.
  • Total JavaScript under 300KB (compressed) for the initial page load.
  • No image over 200KB without explicit approval.

Budgets only work if they are enforced. Integrate performance checks into CI/CD. Flag regressions in pull requests. Make performance visible to the entire team, not just the performance specialists.

Monitoring in production

Set up continuous monitoring with a RUM tool. Popular options include Google’s web-vitals library (free, lightweight), SpeedCurve, Datadog RUM, or New Relic Browser.

Track Core Web Vitals segmented by:

  • Page type. Homepage vs product page vs checkout.
  • Device type. Mobile vs desktop.
  • Geography. Users far from your servers experience different performance.
  • Connection type. 4G vs WiFi vs 3G.

Alert on regressions. A deploy that increases LCP by 500ms should trigger the same urgency as an error rate spike.

What comes next

Knowing what to measure is the first step. Loading performance covers the specific techniques for optimizing the critical rendering path, splitting code effectively, and ensuring resources load in the right order at the right time.

Start typing to search across all content
navigate Enter open Esc close