Back to Blog

Mastering Core Web Vitals: A Performance Guide

Practical strategies for optimizing Core Web Vitals—LCP, FID, and CLS—to improve user experience and SEO rankings

4 min read
PerformanceWeb VitalsSEOOptimizationNext.js

Core Web Vitals have become crucial metrics for both user experience and SEO. Let's explore practical strategies to optimize each metric.

Understanding the Core Web Vitals

Google's Core Web Vitals consist of three key metrics:

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

Optimizing LCP (Largest Contentful Paint)

Goal: Under 2.5 seconds

1. Optimize Images

The largest contentful paint element is often an image. Here's how to optimize:

tsx
import Image from "next/image";

function Hero() {
  return (
    <div className="hero">
      <Image
        src="/hero.jpg"
        alt="Hero image"
        width={1920}
        height={1080}
        priority // Preload above-the-fold images
        placeholder="blur" // Show blur-up effect
        blurDataURL="data:image/jpeg;base64,..." // Tiny base64 preview
      />
    </div>
  );
}

2. Use Resource Hints

tsx
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        {/* Preconnect to external domains */}
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link
          rel="preconnect"
          href="https://fonts.gstatic.com"
          crossOrigin="anonymous"
        />

        {/* Preload critical resources */}
        <link
          rel="preload"
          href="/fonts/inter.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

3. Optimize Server Response Time

typescript
// Use caching headers
export async function GET(request: Request) {
  const data = await fetchData();

  return new Response(JSON.stringify(data), {
    headers: {
      "Content-Type": "application/json",
      "Cache-Control": "public, max-age=3600, s-maxage=86400",
    },
  });
}

Optimizing FID / INP (Interactivity)

Goal: Under 200ms for FID, under 200ms for INP

1. Code Splitting

tsx
import dynamic from "next/dynamic";

// Lazy load heavy components
const AnalyticsDashboard = dynamic(
  () => import("@/components/AnalyticsDashboard"),
  {
    loading: () => <DashboardSkeleton />,
    ssr: false, // Don't render on server if not needed
  }
);

function Dashboard() {
  return (
    <div>
      <Header />
      <AnalyticsDashboard />
    </div>
  );
}

2. Optimize Event Handlers

tsx
import { useCallback } from "react";
import { debounce } from "lodash";

function SearchInput() {
  // Debounce expensive operations
  const handleSearch = useCallback(
    debounce((query: string) => {
      // Expensive search operation
      performSearch(query);
    }, 300),
    []
  );

  return (
    <input
      type="search"
      onChange={(e) => handleSearch(e.target.value)}
      placeholder="Search..."
    />
  );
}

3. Use Web Workers for Heavy Computations

typescript
// worker.ts
self.addEventListener("message", (e) => {
  const result = expensiveCalculation(e.data);
  self.postMessage(result);
});

// component.tsx
useEffect(() => {
  const worker = new Worker("/worker.js");

  worker.postMessage(data);
  worker.onmessage = (e) => {
    setResult(e.data);
  };

  return () => worker.terminate();
}, [data]);

Optimizing CLS (Cumulative Layout Shift)

Goal: Under 0.1

1. Always Include Image Dimensions

tsx
// ❌ Causes layout shift
<img src="/photo.jpg" alt="Photo" />

// ✅ Reserves space
<img
  src="/photo.jpg"
  alt="Photo"
  width="800"
  height="600"
/>

// ✅ Even better with Next.js Image
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
/>

2. Reserve Space for Dynamic Content

css
/* Reserve space for ads or dynamic content */
.ad-container {
  min-height: 250px; /* Expected ad height */
  background: #f0f0f0;
}

/* Use aspect ratio for responsive elements */
.video-container {
  aspect-ratio: 16 / 9;
  width: 100%;
}

3. Avoid Inserting Content Above Existing Content

tsx
// ❌ Inserts content, causing shift
function Feed() {
  return (
    <>
      {newPosts.map((post) => (
        <Post key={post.id} {...post} />
      ))}
      {existingPosts.map((post) => (
        <Post key={post.id} {...post} />
      ))}
    </>
  );
}

// ✅ Show notification instead
function Feed() {
  const [newPosts, setNewPosts] = useState([]);

  return (
    <>
      {newPosts.length > 0 && (
        <button onClick={loadNewPosts}>
          {newPosts.length} new posts available
        </button>
      )}
      {existingPosts.map((post) => (
        <Post key={post.id} {...post} />
      ))}
    </>
  );
}

Monitoring Core Web Vitals

Use the web-vitals library to track metrics in production:

typescript
// app/layout.tsx
"use client";

import { useReportWebVitals } from "next/web-vitals";

export function WebVitals() {
  useReportWebVitals((metric) => {
    // Send to analytics
    gtag("event", metric.name, {
      value: Math.round(
        metric.name === "CLS" ? metric.value * 1000 : metric.value
      ),
      metric_id: metric.id,
      metric_value: metric.value,
      metric_delta: metric.delta,
    });
  });

  return null;
}

Testing Tools

  1. Chrome DevTools - Lighthouse panel for local testing
  2. PageSpeed Insights - Real-world field data
  3. Web Vitals Chrome Extension - Real-time monitoring
  4. WebPageTest - Detailed waterfall analysis

Conclusion

Optimizing Core Web Vitals requires a holistic approach to performance. Focus on the metrics that matter most for your users:

  • Fast loading (LCP)
  • Quick interactivity (FID/INP)
  • Visual stability (CLS)

These optimizations not only improve user experience but also boost SEO rankings. Monitor continuously and iterate based on real-world data.

Remember: Perfect scores aren't the goal—providing a fast, smooth experience for your users is.