<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Leo D</title>
    <description>The latest articles on Forem by Leo D (@leo_d).</description>
    <link>https://forem.com/leo_d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3751175%2Fff437b2a-eff3-4402-a54b-25612ce8f6eb.jpg</url>
      <title>Forem: Leo D</title>
      <link>https://forem.com/leo_d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/leo_d"/>
    <language>en</language>
    <item>
      <title>Tackling Core Web Vitals on a Heavy React App</title>
      <dc:creator>Leo D</dc:creator>
      <pubDate>Tue, 03 Feb 2026 18:54:46 +0000</pubDate>
      <link>https://forem.com/leo_d/tackling-core-web-vitals-on-a-heavy-react-app-4d70</link>
      <guid>https://forem.com/leo_d/tackling-core-web-vitals-on-a-heavy-react-app-4d70</guid>
      <description>&lt;p&gt;Lighthouse 85. PageSpeed “Needs improvement.”&lt;br&gt;
That’s what I had on a &lt;a href="https://faceauraai.com/" rel="noopener noreferrer"&gt;React app with 9 AI tools&lt;/a&gt;, i18n, a dev portal, and a hero slider. Here’s what actually moved the needle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: React Apps Fight CWV by Default
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  LCP: Make the Main Content Load First
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Pick and preload your LCP image
&lt;/h3&gt;

&lt;p&gt;The hero image is usually the LCP. Tell the browser to prioritize it:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;link rel="preload" href="/banner_images/hero.webp" as="image" type="image/webp" fetchpriority="high" /&amp;gt;&lt;/code&gt;&lt;br&gt;
Use rel="preload" and fetchpriority="high" for that single image. Preload only one LCP asset.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use loading and fetchpriority on images
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;// First slide = LCP candidate → eager + high priority&lt;br&gt;
&amp;lt;img loading={index === 0 ? "eager" : "lazy"} fetchpriority={index === 0 ? "high" : "auto"} /&amp;gt;&lt;/code&gt;&lt;br&gt;
First slide: loading="eager" and fetchpriority="high". Later slides: loading="lazy" so they load when visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Preload critical fonts
&lt;/h3&gt;

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

&lt;p&gt;Swap prevents invisible text during font load.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLS: Reserve Space Before Content Loads
&lt;/h2&gt;

&lt;p&gt;Layout shifts come from content appearing after layout is computed. Reserve space first.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use aspect-ratio for all images&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;div style={{ aspectRatio: aspectRatio ||&lt;/code&gt;${width}/${height}&lt;code&gt;}}&amp;gt;&lt;br&gt;
  &amp;lt;img width={width} height={height} ... /&amp;gt;&lt;br&gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Always set width and height (or aspect ratio) on images. The wrapper keeps layout stable before the image loads.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  INP: Lighter Main Thread
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Lazy-load routes and heavy features
&lt;code&gt;// Lazy imports for AI tools, content pages, dashboards
const LazyFaceShapeDetector = lazy(() =&amp;gt; import('../pages/ai-tools/FaceShapeDetector'));
const LazyDeveloperPortal = lazy(() =&amp;gt; import('../pages/DeveloperPortal'));&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only load what’s needed for the current route.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Prefetch on idle
&lt;/h2&gt;

&lt;p&gt;Prefetch likely next routes when the main thread is idle:&lt;br&gt;
&lt;code&gt;if ('requestIdleCallback' in window) {&lt;br&gt;
  requestIdleCallback(() =&amp;gt; {&lt;br&gt;
    routes.forEach(route =&amp;gt; {&lt;br&gt;
      const link = document.createElement('link');&lt;br&gt;
      link.rel = 'prefetch';&lt;br&gt;
      link.href = route;&lt;br&gt;
      document.head.appendChild(link);&lt;br&gt;
    });&lt;br&gt;
  });&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Defer non-critical work
Don’t block first paint with analytics or secondary APIs. Use requestIdleCallback or a lightweight scheduler for non-critical work.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What I’d Do Differently
&lt;/h3&gt;

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

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

&lt;p&gt;Run Lighthouse in Incognito, use WebPageTest for real conditions, and consider web-vitals for RUM. Targets that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LCP: &amp;lt; 2.5s&lt;/li&gt;
&lt;li&gt;CLS: &amp;lt; 0.1&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;INP: &amp;lt; 200ms&lt;br&gt;
One change at a time, then re-measure.&lt;/p&gt;
&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;CWV improvements come from clear priorities:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LCP: Preload the LCP image, set fetchpriority, and optimize fonts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CLS: Use aspect-ratio and placeholders so layout doesn’t jump.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;INP: Lazy-load routes and heavy features, prefetch on idle, defer non-critical work.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These changes improved performance on &lt;a href="https://faceauraai.com/" rel="noopener noreferrer"&gt;FaceAura AI&lt;/a&gt;, an AI-powered style and analysis app built with React, Vite, and Express. The same patterns apply to any heavy React SPA.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>pagespeedinsight</category>
      <category>pageloading</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
