Tools: Tackling Core Web Vitals on a Heavy React App

Tools: Tackling Core Web Vitals on a Heavy React App

Source: Dev.to

The Problem: React Apps Fight CWV by Default ## LCP: Make the Main Content Load First ## 1. Pick and preload your LCP image ## 2. Use loading and fetchpriority on images ## 3. Preload critical fonts ## CLS: Reserve Space Before Content Loads ## INP: Lighter Main Thread ## 2. Prefetch on idle ## What I’d Do Differently ## Summary 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. 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. 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. // 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. 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. Layout shifts come from content appearing after layout is computed. Reserve space first. <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. Only load what’s needed for the current route. 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); }); }); } 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: INP: < 200ms One change at a time, then re-measure. 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. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse - Use aspect-ratio for all images - 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. - 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')); - Defer non-critical work Don’t block first paint with analytics or secondary APIs. Use requestIdleCallback or a lightweight scheduler for non-critical work. - LCP: < 2.5s - 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.