Frontend Engineering/
Lesson

Google introduced Core Web VitalsWhat is core web vitals?Three Google-defined metrics (loading speed, interactivity, visual stability) that measure real-user experience and affect search rankings. as a standardized way to measure real-world user experience. Instead of tracking dozens of synthetic metrics, you focus on three signals: how fast it loads, how quickly it responds, and whether the layout stays stable.

The three metrics at a glance

MetricMeasuresGoodNeeds improvementPoor
LCPLoading speed< 2.5s2.5s, 4s> 4s
INPResponsiveness< 200ms200ms, 500ms> 500ms
CLSVisual stability< 0.10.1, 0.25> 0.25
02

LCPWhat is lcp?Largest Contentful Paint - measures how long it takes for the biggest visible element on the page to finish loading, with a target under 2.5 seconds., Largest Contentful Paint

LCP measures the time until the largest visible element is fully rendered, usually a hero image, heading, or large text block. The most impactful fix is almost always image optimization.

<!-- Bad: large, unoptimized image loaded without priority -->
<img src="hero-5mb.jpg" />

<!-- Good: optimized, responsive image with high fetch priority -->
<img
  src="hero-small.webp"
  srcset="
    hero-small.webp 400w,
    hero-medium.webp 800w,
    hero-large.webp 1200w
  "
  sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
  loading="eager"
  fetchpriority="high"
  width="1200"
  height="600"
/>
fetchpriority="high" tells the browser to load this resource before other images. Use it only on your LCP element, using it everywhere cancels out the benefit.

Common LCP killers

CauseHow to fix it
Unoptimized hero imageCompress, convert to WebP/AVIF, add srcset
Render-blocking CSS/JSInline critical CSS, defer non-critical
Slow server response (TTFB)Use a CDN, optimize server code
Client-side renderingPre-render or SSR the above-fold content
No resource prioritizationAdd fetchpriority="high" to LCP element
03

INPWhat is inp?Interaction to Next Paint - a Core Web Vitals metric measuring how quickly the page responds visually to user interactions., Interaction to Next Paint

INP replaced FID in March 2024. While FID only measured the first interaction's delay, INP tracks all interactions throughout the page lifecycle and reports the worst one. An interaction is a click, tap, or key press, INP measures the time from input to visual response.

INP is almost always caused by JavaScript blocking the main thread. Code splittingWhat is code splitting?Breaking your application into smaller JavaScript chunks that load on demand so users only download the code needed for the page they're viewing. is your primary weapon:

// Bad: loading everything up front blocks the main thread
import HeavyChart from './HeavyChart';
import ComplexTable from './ComplexTable';
import PDFViewer from './PDFViewer';

function Dashboard() {
  return (
    <div>
      <HeavyChart />    {/* 200KB added to initial bundle */}
      <ComplexTable />   {/* 150KB added to initial bundle */}
      <PDFViewer />      {/* 300KB added to initial bundle */}
    </div>
  );
}

// Good: lazy load heavy components
import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));
const ComplexTable = lazy(() => import('./ComplexTable'));
const PDFViewer = lazy(() => import('./PDFViewer'));

function Dashboard() {
  return (
    <div>
      <Suspense fallback={<div>Loading chart...</div>}>
        <HeavyChart />
      </Suspense>
      <Suspense fallback={<div>Loading table...</div>}>
        <ComplexTable />
      </Suspense>
      <Suspense fallback={<div>Loading viewer...</div>}>
        <PDFViewer />
      </Suspense>
    </div>
  );
}
AI pitfall
AI never code-splits. A dashboard with three heavy components might ship a 700KB initial bundle when it could ship 50KB and load the rest on demand.

Diagnosing INP issues

PhaseWhat happensHow to fix
Input delayJS is already running when user clicksReduce long tasks, code-split
Processing timeEvent handler takes too longOptimize handler, debounce, use web workers
Presentation delayBrowser takes too long to paintReduce DOM size, simplify CSS
04

CLSWhat is cls?Cumulative Layout Shift - measures how much the page layout moves around unexpectedly while loading, with a target score under 0.1., Cumulative Layout Shift

CLS measures unexpected visual instability, content jumping around as the page loads. The most common cause is images without explicit dimensions.

<!-- Bad: browser doesn't know this image is 600px tall -->
<img src="photo.jpg" />

<!-- Good: browser reserves space before the image loads -->
<img src="photo.jpg" width="800" height="600" />

<!-- Also good: CSS aspect-ratio prevents shift -->
<div style="aspect-ratio: 4/3; overflow: hidden;">
  <img src="photo.jpg" style="width: 100%; height: auto;" />
</div>

Fonts are another common CLS culprit. When the browser swaps a fallback font for a web font, text reflowing causes a visible shift.

/* Use font-display: optional to prevent layout shift from fonts */
@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: optional; /* No shift - if font isn't cached, skip it */
}
AI pitfall
AI-generated components almost never include width and height on images. Every <img> tag AI produces will cause CLS unless you add dimensions.

Other common CLS causes

CauseFix
Images without dimensionsAdd width and height attributes
Ads or embeds without reserved spaceUse aspect-ratio containers or min-height
Dynamically injected contentReserve space with skeleton placeholders
Web font swapUse font-display: optional or swap with size-adjust
05

Measuring Core Web VitalsWhat is core web vitals?Three Google-defined metrics (loading speed, interactivity, visual stability) that measure real-user experience and affect search rankings.

ToolTypeBest for
Chrome DevTools / LighthouseLabDevelopment and debugging
PageSpeed InsightsLab + FieldQuick check on any URL
Chrome UX Report (CrUX)FieldReal-world data from Chrome users
web-vitals libraryFieldMonitoring in your own production analytics

Lab data tells you what could happen. Field data tells you what is actually happening. Field data is what Google uses for ranking.

import { onCLS, onINP, onLCP } from 'web-vitals';

function sendToAnalytics({ name, value, id }) {
  // Send to your analytics service
  fetch('/api/vitals', {
    method: 'POST',
    body: JSON.stringify({ name, value, id }),
  });
}

onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
06

Where to start

If you have poor scores across all three metrics, fix them in this order:

  1. CLSWhat is cls?Cumulative Layout Shift - measures how much the page layout moves around unexpectedly while loading, with a target score under 0.1. first: usually a quick fix, just add image dimensions and font-display
  2. LCPWhat is lcp?Largest Contentful Paint - measures how long it takes for the biggest visible element on the page to finish loading, with a target under 2.5 seconds. second: optimize your hero image, add fetchpriority, check server response time
  3. INPWhat is inp?Interaction to Next Paint - a Core Web Vitals metric measuring how quickly the page responds visually to user interactions. last: code splittingWhat is code splitting?Breaking your application into smaller JavaScript chunks that load on demand so users only download the code needed for the page they're viewing. takes more thought but has the biggest impact on interactivity
javascript
// Complete example: Optimizing for Core Web Vitals

// 1. LCP optimization, responsive hero with priority
function HeroSection() {
  return (
    <section>
      <img
        src="/hero.webp"
        srcSet="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w"
        sizes="100vw"
        width="1200"
        height="600"
        alt="Hero image"
        fetchpriority="high"
        loading="eager"
      />
      <h1>Welcome to Our Site</h1>
    </section>
  );
}

// 2. INP optimization with code splitting
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <div>
      <header>Navigation</header>
      <main>Main content</main>
      <Suspense fallback={<div>Loading...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

// 3. CLS optimization, always set dimensions
function ProductImage({ src, alt }) {
  return (
    <div style={{ aspectRatio: '16/9', overflow: 'hidden' }}>
      <img
        src={src}
        alt={alt}
        width="800"
        height="450"
        loading="lazy"
        style={{ width: '100%', height: 'auto' }}
      />
    </div>
  );
}

// 4. Monitoring in production
import { onCLS, onINP, onLCP } from 'web-vitals';

function reportWebVitals(metric) {
  if (metric.rating === 'poor') {
    console.warn(`Poor ${metric.name}:`, metric.value);
  }
}

onCLS(reportWebVitals);
onINP(reportWebVitals);
onLCP(reportWebVitals);