DEV Community

Japheth Gonzales
Japheth Gonzales

Posted on Originally published at japhethgonzales.com

Practical Guide to Optimizing Core Web Vitals in React & Next.js

Cover Image

Practical Guide to Optimizing Core Web Vitals in React & Next.js

Performance is no longer just a nice-to-have feature in modern web development—it directly impacts user engagement, conversion rates, and search engine rankings. Google's Core Web Vitals (CWV) provide clear, user-centric metrics to quantify performance: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS).

While React and Next.js provide excellent abstractions, poor architectural choices can lead to slow renders, layout shifts, and sluggish user interactions. In this guide, we will explore practical, high-impact strategies to measure and optimize each metric in your React and Next.js applications.


1. Understanding the Core Web Vitals Metrics

  • LCP (Largest Contentful Paint): Measures loading performance. To provide a good user experience, LCP should occur within 2.5 seconds of when the page first starts loading.
  • INP (Interaction to Next Paint): Replaced FID in March 2024. Measures overall page responsiveness by evaluating the latency of user interactions (clicks, taps, keypresses). Aim for 200 milliseconds or less.
  • CLS (Cumulative Layout Shift): Measures visual stability. Pages should maintain a CLS of 0.1 or less to avoid accidental clicks and jarring layout jumps.

2. Optimizing Largest Contentful Paint (LCP)

LCP is usually driven by hero images, large background graphics, or critical text blocks. Here is how to speed it up:

Prioritize Hero Images with next/image

Always mark your above-the-fold hero images with priority to disable lazy loading and instruct Next.js to preload them:

import Image from 'next/image';

export default function HeroSection() {
  return (
    <div className="hero">
      <Image
        src="/hero-banner.jpg"
        alt=" Hero Banner"
        width={1200}
        height={600}
        priority // Forces immediate preloading
        sizes="(max-width: 768px) 100vw, 1200px"
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Optimize Web Fonts

Unoptimized custom web fonts can block text rendering (causing FOIT/FOUT). Use next/font to automatically inline font CSS and self-host font files at build time:

import { Inter } from 'next/font/google';

const inter = Inter({ 
  subsets: ['latin'],
  display: 'swap',
});

export default function Layout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Fixing Interaction to Next Paint (INP)

INP measures how quickly a page responds to user actions. Long synchronous Javascript execution blocks the main thread, causing poor INP scores.

Yield to the Main Thread with useTransition

When dealing with heavy state updates (like filtering a long list), use React 18's useTransition to prioritize user input over expensive UI re-renders:

import { useState, useTransition } from 'react';

export function SearchFilter({ items }) {
  const [query, setQuery] = useState('');
  const [filteredList, setFilteredList] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setQuery(value); // Immediate update for input responsiveness

    startTransition(() => {
      // Non-urgent transition update
      const results = items.filter(item => item.name.includes(value));
      setFilteredList(results);
    });
  };

  return (
    <div>
      <input value={query} onChange={handleSearch} placeholder="Search..." />
      {isPending && <p>Updating results...</p>}
      <ul>
        {filteredList.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Code-Splitting Heavy Client Libraries

Avoid loading large libraries (e.g., dynamic chart renderers or rich-text editors) on initial load. Dynamically import them only when needed:

import dynamic from 'next/dynamic';

const HeavyChartComponent = dynamic(() => import('@/components/HeavyChart'), {
  ssr: false,
  loading: () => <p>Loading chart...</p>,
});
Enter fullscreen mode Exit fullscreen mode

4. Preventing Cumulative Layout Shift (CLS)

CLS happens when DOM elements shift as resources load asynchronously.

Reserve Space for Dynamic Content & Ads

Always define width and height attributes or explicit CSS aspect ratios on images, embeds, and dynamic slots to reserve layout space before content renders:

.ad-container {
  min-height: 250px;
  width: 100%;
  background-color: #f3f4f6; /* Placeholder space */
}
Enter fullscreen mode Exit fullscreen mode

Avoid Unsized Skeleton Loaders

Ensure skeleton loaders match the exact dimensions of the expected content to prevent sudden layout jumps when real data hydrates.


5. Monitoring Core Web Vitals

To ensure your optimizations are effective, monitor real-user metrics (RUM):

  • Chrome DevTools: Use the Performance and Lighthouse tabs for local diagnostics.
  • Vercel Speed Insights / Next.js Analytics: Automatically track field data directly from real visitors.
  • useReportWebVitals Hook: Log metrics directly inside Next.js:
// app/use-web-vitals.ts
'use client';
import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    console.log(metric);
  });
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Optimizing Core Web Vitals in React and Next.js is an ongoing process of measuring, refining, and monitoring. By prioritizing critical assets for LCP, deferring non-essential script work for INP, and explicitly sizing dynamic space for CLS, you can deliver blazing-fast, delight-inducing user experiences.


Originally published on japhethgonzales.com by Japheth Gonzales (AI Agent Development Operations Manager @ Ensight).

Top comments (0)