Tackling Core Web Vitals on a Heavy React App
Source: Dev.to
Introduction
A heavy React SPA with multiple AI tools, i18n, a developer portal, and a hero slider often scores Lighthouse 85 with a “Needs improvement” PageSpeed rating. The main culprits are large JavaScript bundles, fonts, i18n resources, and above‑the‑fold images that compete for network and main‑thread time, leading to slow LCP, layout shifts (CLS), and sluggish interactions (INP). Small, targeted changes to how assets are loaded and rendered can move the needle dramatically.
Prioritizing the LCP Image
The hero image is usually the LCP candidate. Tell the browser to preload it with high priority:
<!-- Example preload markup (replace with actual image URL) -->
<link rel="preload" as="image" href="/path/to/hero.jpg" fetchpriority="high">
- First slide (the LCP):
loading="eager"andfetchpriority="high". - Subsequent slides:
loading="lazy"so they load only when visible.
Optimizing Font Loading
Fonts block text rendering. Preload the WOFF2 file and use font-display: swap to avoid invisible text:
<link rel="preload" href="https://fonts.gstatic.com/s/inter/.../Inter.woff2" as="font" type="font/woff2" crossorigin>
@font-face {
font-family: 'Inter';
src: url('https://fonts.gstatic.com/s/inter/.../Inter.woff2') format('woff2');
font-weight: 400;
font-display: swap; /* Show fallback text until the font loads */
}
Preventing Layout Shifts
Layout shifts occur when content appears after the initial layout has been computed. Reserve space ahead of time.
Use aspect-ratio for Images
{/* Image component */}
<img
src={src}
alt={alt}
style={{ aspectRatio: `${width}/${height}` }}
loading="eager"
fetchpriority="high"
/>
- Always set
widthandheight(oraspect-ratio) on images. - The wrapper keeps the layout stable before the image loads.
Placeholders for Lazy Content
{!isLoaded && (
<div className="placeholder" style={{ width, height }} />
)}
<img
src={src}
onLoad={() => setIsLoaded(true)}
style={{ opacity: isLoaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
- The placeholder reserves space.
- An opacity transition avoids visual jumps.
Lazy‑Loading 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'));
- Load only what’s needed for the current route.
- Prefetch likely next routes when the main thread is idle:
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('../pages/NextRoute');
});
}
Deferring Non‑Critical Work
- Move analytics and secondary API calls off the critical path.
- Use
requestIdleCallbackor a lightweight scheduler for tasks that aren’t needed for the first paint.
i18n and Bundle Splitting
- Load the default locale initially; lazy‑load other locales on demand.
- With Vite, use
manualChunksto split vendor bundles, keeping i18n and UI libraries in separate chunks.
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('i18next')) return 'i18n';
if (id.includes('react')) return 'react-vendor';
}
}
}
}
}
});
Testing and Targets
- Run Lighthouse in an incognito window.
- Use WebPageTest for real‑world conditions.
- Consider the web‑vitals library for real‑user monitoring (RUM).
Core Web Vitals targets
| Metric | Target |
|---|---|
| LCP | < 2.5 s |
| CLS | < 0.1 |
| INP | < 200 ms |
Summary of Improvements
| Area | What to Do |
|---|---|
| LCP | Preload the LCP image, set fetchpriority="high", and optimize fonts. |
| CLS | Use aspect-ratio or explicit width/height, and placeholders to keep layout stable. |
| INP | Lazy‑load routes and heavy features, prefetch on idle, and defer non‑critical work. |
Applying these patterns to FaceAura AI (an AI‑powered style and analysis app built with React, Vite, and Express) yielded noticeable performance gains. The same techniques work for any heavyweight React SPA.
Quick start
- Identify and preload the LCP image.
- Preload fonts with
font-display: swap. - Add
aspect-ratio/width‑height and placeholders for above‑the‑fold media. - Lazy‑load routes and heavy modules, prefetch likely next routes during idle time.
These steps usually deliver the biggest impact on Core Web Vitals.