DEV Community

Cover image for Tackling Core Web Vitals on a Heavy React App
Leo D
Leo D

Posted on

Tackling Core Web Vitals on a Heavy React App

Lighthouse 85. PageSpeed “Needs improvement.”
That’s what I had on a React app with 9 AI tools, i18n, a dev portal, and a hero slider. Here’s what actually moved the needle.

The Problem: React Apps Fight CWV by Default

SPAs load a lot before they’re usable: JS bundles, fonts, i18n, providers. Above-the-fold images compete with that. The result: slow LCP, layout jumps (CLS), and sluggish interactions (INP). The fixes are small changes in how you load and render assets.

LCP: Make the Main Content Load First

1. Pick and preload your LCP image

The hero image is usually the LCP. Tell the browser to prioritize it:

<link rel="preload" href="/banner_images/hero.webp" as="image" type="image/webp" fetchpriority="high" />
Use rel="preload" and fetchpriority="high" for that single image. Preload only one LCP asset.

2. Use loading and fetchpriority on images

// First slide = LCP candidate → eager + high priority
<img loading={index === 0 ? "eager" : "lazy"} fetchpriority={index === 0 ? "high" : "auto"} />

First slide: loading="eager" and fetchpriority="high". Later slides: loading="lazy" so they load when visible.

3. Preload critical fonts

Fonts block text render. Preload WOFF2 and use font-display: swap:
<link rel="preload" href="https://fonts.gstatic.com/s/inter/.../Inter.woff2" as="font" type="font/woff2" crossorigin />
@font-face {
font-family: 'Inter';
font-display: swap;
src: url(...);
}

Swap prevents invisible text during font load.

CLS: Reserve Space Before Content Loads

Layout shifts come from content appearing after layout is computed. Reserve space first.

  1. Use aspect-ratio for all images

<div style={{ aspectRatio: aspectRatio ||${width}/${height}}}>
<img width={width} height={height} ... />
</div>

Always set width and height (or aspect ratio) on images. The wrapper keeps layout stable before the image loads.

  1. Use placeholders for lazy content When content loads later, reserve space: {!isLoaded && ( <div className="absolute inset-0 bg-gray-100 animate-pulse" style={{ width: '100%', height: '100%' }} /> )} <img className={isLoaded ? 'opacity-100' : 'opacity-0'} ... onLoad={() => setIsLoaded(true)} /> Opacity transition keeps the layout fixed and avoids layout shifts.

INP: Lighter Main Thread

  1. Lazy-load routes and heavy features // Lazy imports for AI tools, content pages, dashboards const LazyFaceShapeDetector = lazy(() => import('../pages/ai-tools/FaceShapeDetector')); const LazyDeveloperPortal = lazy(() => import('../pages/DeveloperPortal'));

Only load what’s needed for the current route.

2. Prefetch on idle

Prefetch likely next routes when the main thread is idle:
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
routes.forEach(route => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = route;
document.head.appendChild(link);
});
});
}

  1. Defer non-critical work Don’t block first paint with analytics or secondary APIs. Use requestIdleCallback or a lightweight scheduler for non-critical work.

What I’d Do Differently

i18n: Load only the default language initially; lazy-load other locales.
Hero slider: Don’t load all 5 slides at once; load slides 2–5 only when near the viewport.

Vite: Use manualChunks to split vendor bundles; i18n and UI libs can be separate chunks.
Measure, Don’t Guess

Run Lighthouse in Incognito, use WebPageTest for real conditions, and consider web-vitals for RUM. Targets that matter:

  • LCP: < 2.5s
  • CLS: < 0.1
  • INP: < 200ms
    One change at a time, then re-measure.

    Summary

    CWV improvements come from clear priorities:

  • LCP: Preload the LCP image, set fetchpriority, and optimize fonts.

  • CLS: Use aspect-ratio and placeholders so layout doesn’t jump.

  • INP: Lazy-load routes and heavy features, prefetch on idle, defer non-critical work.

These changes improved performance on FaceAura AI, an AI-powered style and analysis app built with React, Vite, and Express. The same patterns apply to any heavy React SPA.

If you’re optimizing CWV on a React app, start with the LCP image and font loading, then add aspect-ratio and placeholders. Those will usually have the biggest impact.

Top comments (0)