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.
- 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.
- 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
- 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);
});
});
}
- 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)