Skip to content
All essays
WebFebruary 25, 202410 min

Web Vitals Optimization

Master Google Core Web Vitals to improve user experience and search rankings.

Ü
Ümit Uz
Mobile & Full Stack Developer

Core Web Vitals are the foundation of user experience on the web. Master them to improve both UX and SEO.

What are Core Web Vitals?

Google's Core Web Vitals measure real-world user experience:

  1. 1LCP (Largest Contentful Paint): Loading performance
  2. 2FID (First Input Delay): Interactivity
  3. 3CLS (Cumulative Layout Shift): Visual stability

Largest Contentful Paint (LCP)

What it Measures

Time from navigation to when the largest content element renders.

Target: < 2.5 seconds

Measure LCP

javascript
// Measure LCP
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP:', lastEntry.startTime);
  console.log('LCP Element:', lastEntry.element);
}).observe({ entryTypes: ['largest-contentful-paint'] });

Optimization Strategies

1. Optimize Images

html
<!-- Use modern formats -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" loading="eager" decoding="async">
</picture>

<!-- Add dimensions to prevent layout shift -->
<img
  src="image.jpg"
  width="800"
  height="600"
  loading="lazy"
>

2. Preload Critical Resources

html
<!-- Preload LCP image -->
<link rel="preload" as="image" href="hero.jpg">

<!-- Preload critical CSS -->
<link rel="preload" as="style" href="critical.css">

<!-- Preconnect to origins -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">

3. Eliminate Render-Blocking Resources

html
<!-- Defer non-critical CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>

<!-- Defer JavaScript -->
<script defer src="app.js"></script>

4. Server-Side Rendering

Generate HTML on the server for faster initial render.

javascript
// Next.js example
export async function getServerSideProps() {
  const data = await fetchData();
  return {
    props: { data }, // Server-rendered HTML
  };
}

First Input Delay (FID)

What it Measures

Time from user's first interaction to browser response.

Target: < 100 milliseconds

Measure FID

javascript
// Measure FID
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('FID:', entry.processingStart - entry.startTime);
  }
}).observe({ entryTypes: ['first-input'] });

Optimization Strategies

1. Reduce JavaScript Execution

javascript
// Code split by route
const Dashboard = lazy(() => import('./routes/Dashboard'));

// Lazy load components
const HeavyChart = lazy(() => import('./HeavyChart'));

// Use Web Workers for heavy computation
const worker = new Worker('worker.js');
worker.postMessage({ data: largeDataSet });

2. Defer Non-Critical JavaScript

html
<!-- Defer non-critical JS -->
<script defer src="analytics.js"></script>
<script defer src="chat-widget.js"></script>

<!-- Load critical JS inline -->
<script>
  // Critical only
  initNavigation();
</script>

3. Use React Concurrent Features

javascript
// Use Transitions for non-urgent updates
import { startTransition } from 'react';

startTransition(() => {
  setSearchResults(filterLargeList(query));
});

// Use Suspense for lazy loading
<Suspense fallback={<Spinner />}>
  <HeavyComponent />
</Suspense>

Cumulative Layout Shift (CLS)

What it Measures

Unexpected layout shifts during page load.

Target: < 0.1

Measure CLS

javascript
// Measure CLS
let clsValue = 0;
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
      console.log('CLS:', clsValue);
    }
  }
}).observe({ entryTypes: ['layout-shift'] });

Optimization Strategies

1. Reserve Space for Dynamic Content

css
/* Reserve space for images */
.image-container {
  aspect-ratio: 16 / 9;
  min-height: 300px;
  background: #f0f0f0;
}

/* Reserve space for ads */
.ad-slot {
  min-height: 250px;
}

2. Include Image Dimensions

html
<!-- Always include width and height -->
<img
  src="image.jpg"
  width="800"
  height="600"
  style="aspect-ratio: 800/600;"
>

3. Avoid Inserting Content Above Existing

javascript
// Bad: Pushes content down
function pushNotification(message) {
  const notification = document.createElement('div');
  document.body.prepend(notification);
}

// Good: Fixed position overlay
function showNotification(message) {
  const notification = document.createElement('div');
  notification.style.position = 'fixed';
  notification.style.top = '20px';
  notification.style.right = '20px';
  document.body.appendChild(notification);
}

4. Use font-display: swap

css
@font-face {
  font-family: 'Custom Font';
  src: url('font.woff2') format('woff2');
  font-display: swap; /* Prevents FOIT */
}

Additional Web Vitals

Time to First Byte (TTFB)

Target: < 800 milliseconds

javascript
// Measure TTFB
const navigation = performance.getEntriesByType('navigation')[0];
const ttfb = navigation.responseStart - navigation.requestStart;
console.log('TTFB:', ttfb);

Optimization:

  • Use CDN
  • Enable compression
  • Optimize server code
  • Use HTTP/2 or HTTP/3

First Contentful Paint (FCP)

Target: < 1.8 seconds

javascript
// Measure FCP
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name === 'first-contentful-paint') {
      console.log('FCP:', entry.startTime);
    }
  }
}).observe({ entryTypes: ['paint']});

Optimization:

  • Reduce render-blocking resources
  • Minify CSS and JavaScript
  • Critical CSS inlining

Monitoring and Reporting

Real User Monitoring (RUM)

javascript
// Send Web Vitals to analytics
import { getCLS, getFID, getLCP } from 'web-vitals';

getCLS((metric) => sendToAnalytics(metric));
getFID((metric) => sendToAnalytics(metric));
getLCP((metric) => sendToAnalytics(metric));

function sendToAnalytics(metric) {
  fetch('/api/analytics', {
    method: 'POST',
    body: JSON.stringify({
      name: metric.name,
      value: metric.value,
      id: metric.id,
    }),
  });
}

Lighthouse CI

yaml
# .github/workflows/lighthouse.yml
name: Lighthouse CI

on: [push]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v9
        with:
          urls: |
            https://your-site.com
          uploadArtifacts: true
          temporaryPublicStorage: true

Common Issues and Solutions

Large LCP

Issue: Hero image takes too long to load

Solutions:

html
<!-- 1. Preload hero image -->
<link rel="preload" as="image" href="hero.webp">

<!-- 2. Use AVIF/WebP -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero">
</picture>

<!-- 3. Add width/height -->
<img src="hero.jpg" width="1200" height="630" alt="Hero">

Poor FID

Issue: Too much JavaScript on main thread

Solutions:

javascript
// 1. Code split
const HeavyComponent = lazy(() => import('./HeavyComponent'));

// 2. Defer non-critical JS
<script defer src="analytics.js"></script>

// 3. Use Web Workers
const worker = new Worker('compute.worker.js');

High CLS

Issue: Images without dimensions

Solutions:

html
<!-- 1. Always add dimensions -->
<img src="image.jpg" width="800" height="600">

<!-- 2. Use aspect-ratio -->
<img src="image.jpg" style="aspect-ratio: 4/3;">

<!-- 3. Reserve space -->
<div style="aspect-ratio: 16/9; background: #f0f0f0;">
  <img src="image.jpg" style="width: 100%; height: 100%; object-fit: cover;">
</div>

Testing Tools

Lighthouse

bash
# Run Lighthouse
lighthouse https://your-site.com --view

# Run in CI
npm run lighthouse

PageSpeed Insights

bash
# Test with PSI
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://your-site.com"

Chrome DevTools

  1. 1Open DevTools
  2. 2Go to Performance tab
  3. 3Record page load
  4. 4Analyze Web Vitals

Best Practices

DO

  • Measure real user performance
  • Optimize for Core Web Vitals
  • Use modern image formats
  • Lazy load non-critical resources
  • Monitor continuously
  • Test on real devices

DON'T

  • Optimize without measuring
  • Ignore CLS
  • Block rendering with large CSS/JS
  • Serve unoptimized images
  • Forget about mobile performance
  • Assume lab scores equal real-world

Conclusion

Core Web Vitals are essential for both user experience and SEO. Measure first, then optimize. Focus on LCP by optimizing resources, improve FID by reducing JavaScript, and minimize CLS by reserving space for content. Monitor continuously and iterate based on real user data.

Related essays

Next essay
SEO for Single Page Apps