<?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: Shudhanshu Raj</title>
    <description>The latest articles on Forem by Shudhanshu Raj (@shudhanshuraj).</description>
    <link>https://forem.com/shudhanshuraj</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%2F835458%2Fdfa2040b-b82a-401c-bb11-07b5683f7b27.png</url>
      <title>Forem: Shudhanshu Raj</title>
      <link>https://forem.com/shudhanshuraj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/shudhanshuraj"/>
    <language>en</language>
    <item>
      <title>Micro-Frontend Architecture: How to Split a Monolith Without Losing Your Mind</title>
      <dc:creator>Shudhanshu Raj</dc:creator>
      <pubDate>Tue, 19 May 2026 04:40:00 +0000</pubDate>
      <link>https://forem.com/shudhanshuraj/micro-frontend-architecture-how-to-split-a-monolith-without-losing-your-mind-217e</link>
      <guid>https://forem.com/shudhanshuraj/micro-frontend-architecture-how-to-split-a-monolith-without-losing-your-mind-217e</guid>
      <description>&lt;p&gt;I still remember the moment I knew our frontend had become a problem.&lt;/p&gt;

&lt;p&gt;It took &lt;strong&gt;4 minutes&lt;/strong&gt; to run our test suite. PRs sat in review for days because everyone was afraid of merge conflicts. Deploying a button color change required coordinating with three other teams. We had a 280,000-line React monolith, and it was slowly eating us alive.&lt;/p&gt;

&lt;p&gt;That was three years ago. Since then I've navigated two micro-frontend migrations, made some expensive mistakes, and developed strong opinions about what actually works in production. This is that story.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Even Is a Micro-Frontend?
&lt;/h2&gt;

&lt;p&gt;The idea is simple: apply the same thinking that gave us microservices — independent, separately deployable units — to the frontend.&lt;/p&gt;

&lt;p&gt;Instead of one giant React (or Angular, or Vue) app that every team commits to, you break the UI into smaller apps owned by individual teams. Each team ships independently. Each slice of the UI has its own repo, its own CI/CD pipeline, its own release cadence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEFORE (Monolith):
┌─────────────────────────────────────────────┐
│              one-giant-app                  │
│   Header | Dashboard | Orders | Settings    │
│        (everyone touches everything)        │
└─────────────────────────────────────────────┘

AFTER (Micro-Frontends):
┌──────────┐  ┌───────────┐  ┌────────┐  ┌──────────┐
│  Shell   │  │ Dashboard │  │ Orders │  │ Settings │
│  (host)  │  │   (MFE)   │  │ (MFE)  │  │  (MFE)   │
└──────────┘  └───────────┘  └────────┘  └──────────┘
   Team A         Team B        Team C      Team D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the pitch. Reality, as always, is more complicated.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Three Integration Strategies (and When to Use Each)
&lt;/h2&gt;

&lt;p&gt;How you stitch micro-frontends together is the most consequential architectural decision you'll make. Get this wrong and you'll create more problems than you solve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy 1: Build-Time Integration
&lt;/h3&gt;

&lt;p&gt;Each MFE is published as an npm package. The shell app imports and bundles them together at build time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shell/package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@company/dashboard-mfe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.4.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@company/orders-mfe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;"1.9.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@company/settings-mfe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"3.1.2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The catch:&lt;/strong&gt; This isn't really micro-frontend architecture. It's just modular architecture with extra steps. To deploy the Orders team's new feature, the Shell team still has to bump a package version and redeploy. You've added npm overhead but kept the deployment coupling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; Teams are small (or even solo), and you want better code organization without operational complexity. Don't call it micro-frontends.&lt;/p&gt;




&lt;h3&gt;
  
  
  Strategy 2: Run-Time Integration via iframes
&lt;/h3&gt;

&lt;p&gt;The oldest trick in the book. Each MFE lives at its own URL and gets embedded in an iframe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- shell/index.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"dashboard-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://dashboard.internal.company.com"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The surprising truth:&lt;/strong&gt; iframes solve almost every hard micro-frontend problem out of the box. Perfect isolation. Independent deployments. No shared global state. Different tech stacks? No problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real costs:&lt;/strong&gt; Cross-frame communication is painful (&lt;code&gt;postMessage&lt;/code&gt; everywhere). Responsive layouts are a nightmare. Accessibility is genuinely hard to get right — focus management, screen readers, keyboard traps. And they &lt;em&gt;feel&lt;/em&gt; clunky to users if you're not careful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; You need hard isolation (e.g., embedding a third-party widget), or your MFEs are truly standalone tools that don't need to interact much.&lt;/p&gt;




&lt;h3&gt;
  
  
  Strategy 3: Run-Time Integration via Module Federation (The Modern Approach)
&lt;/h3&gt;

&lt;p&gt;Webpack 5 (and now Vite with plugins) introduced &lt;strong&gt;Module Federation&lt;/strong&gt; — the ability for one JavaScript bundle to dynamically load code from another bundle &lt;em&gt;at runtime&lt;/em&gt;, across separate deployments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// dashboard-mfe/webpack.config.js&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ModuleFederationPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboardMFE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remoteEntry.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;exposes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// shell/webpack.config.js&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ModuleFederationPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shell&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;remotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dashboardMFE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboardMFE@https://dashboard.cdn.company.com/remoteEntry.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the shell can do this at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shell/src/App.jsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Dashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboardMFE/Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Suspense&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spinner&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Dashboard&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Suspense&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Dashboard team deploys their new &lt;code&gt;remoteEntry.js&lt;/code&gt; to the CDN. The shell picks it up &lt;strong&gt;on the next page load — no shell redeploy needed&lt;/strong&gt;. That's the magic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Five Problems Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;Theory is easy. Here's where things get genuinely hard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 1: Dependency Hell
&lt;/h3&gt;

&lt;p&gt;Each MFE has its own &lt;code&gt;node_modules&lt;/code&gt;. Without careful coordination, you'll end up loading React 18.2 &lt;em&gt;and&lt;/em&gt; React 18.3 in the same page. Or worse, two different major versions.&lt;/p&gt;

&lt;p&gt;Module Federation's &lt;code&gt;shared&lt;/code&gt; config handles this, but it needs explicit care:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// Only one instance allowed on the page&lt;/span&gt;
    &lt;span class="na"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^18.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;eager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Load lazily, not in initial bundle&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requiredVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^18.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;singleton: true&lt;/code&gt; is non-negotiable for React. Two React instances on one page will break context, hooks, and refs in subtle, maddening ways. Add the shared config, then &lt;strong&gt;enforce it&lt;/strong&gt; — a lint rule or CI check that flags any MFE shipping its own React bundle.&lt;/p&gt;




&lt;h3&gt;
  
  
  Problem 2: Shared State Is a Trap
&lt;/h3&gt;

&lt;p&gt;The most tempting antipattern: a global Redux store or Context shared across MFEs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Don't do this&lt;/span&gt;
&lt;span class="c1"&gt;// shell/src/globalStore.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// orders-mfe/src/OrderList.jsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shell/globalStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// tight coupling disguised as convenience&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've just recreated the monolith in a trenchcoat. Now the Orders MFE can't deploy without the shell, because it's importing from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do instead:&lt;/strong&gt; Keep each MFE's state local. Share the &lt;em&gt;minimum necessary&lt;/em&gt; via well-defined contracts: URL parameters, custom events, or a thin event bus.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Lightweight event bus for cross-MFE communication&lt;/span&gt;
&lt;span class="c1"&gt;// shell/src/eventBus.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventTarget&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emit&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;off&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// orders-mfe: emit an event when an order is placed&lt;/span&gt;
&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order:placed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;12345&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;49.99&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// notifications-mfe: listen for it&lt;/span&gt;
&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order:placed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;showToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; confirmed!`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, decoupled, and each MFE can be deployed in isolation.&lt;/p&gt;




&lt;h3&gt;
  
  
  Problem 3: Design System Drift
&lt;/h3&gt;

&lt;p&gt;Without discipline, each MFE will slowly develop its own button component, its own color tokens, its own spacing system. Six months in, your app looks like it was designed by five people who never spoke to each other — because it was.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; A shared design system as a separate, versioned package. But version it carefully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@company/design-system@3.x — locked via shared config in Module Federation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design system is the one dependency that &lt;em&gt;should&lt;/em&gt; be shared. Everything else, keep isolated.&lt;/p&gt;




&lt;h3&gt;
  
  
  Problem 4: Performance — You're Loading More JavaScript
&lt;/h3&gt;

&lt;p&gt;A micro-frontend architecture almost always means more JavaScript on the page. Multiple &lt;code&gt;remoteEntry.js&lt;/code&gt; files, multiple framework instances if you're not careful, multiple routers.&lt;/p&gt;

&lt;p&gt;Track your bundle metrics religiously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Add to your CI pipeline — fail if initial JS exceeds budget&lt;/span&gt;
&lt;span class="c1"&gt;// webpack.config.js&lt;/span&gt;
&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;maxEntrypointSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;170000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// 170KB gzipped&lt;/span&gt;
  &lt;span class="nx"&gt;maxAssetSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;hints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// Fail the build, don't just warn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And lean on lazy loading aggressively. Users shouldn't pay the cost of loading the Settings MFE when they're on the Dashboard.&lt;/p&gt;




&lt;h3&gt;
  
  
  Problem 5: Testing Gets Complicated
&lt;/h3&gt;

&lt;p&gt;Unit tests per MFE are easy — same as before. But integration testing across MFE boundaries is where teams struggle.&lt;/p&gt;

&lt;p&gt;My recommendation: &lt;strong&gt;contract testing&lt;/strong&gt;. Each MFE publishes a contract describing its interface (the events it emits, the props it accepts). The shell validates against those contracts in CI, without needing to spin up every MFE.&lt;/p&gt;

&lt;p&gt;Tools like &lt;a href="https://pact.io/" rel="noopener noreferrer"&gt;Pact&lt;/a&gt; handle this well, or you can roll a simple JSON schema validation in your CI pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture I'd Recommend Today
&lt;/h2&gt;

&lt;p&gt;For a mid-size product team (3–6 frontend teams), here's my pragmatic setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                     Shell App (Host)                    │
│  - Routing                                              │
│  - Auth / session                                       │
│  - Design system tokens                                 │
│  - Event bus                                            │
└──────┬──────────────┬──────────────┬────────────────────┘
       │              │              │
  Module Fed     Module Fed     Module Fed
       │              │              │
┌──────▼──────┐ ┌─────▼──────┐ ┌────▼──────────┐
│  Dashboard  │ │   Orders   │ │   Settings    │
│    MFE      │ │    MFE     │ │     MFE       │
│  (Team B)   │ │  (Team C)  │ │   (Team D)   │
└─────────────┘ └────────────┘ └───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Shell responsibilities:&lt;/strong&gt; routing, authentication, session, shared design system, event bus. Nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MFE responsibilities:&lt;/strong&gt; everything inside their domain boundary. Own their data fetching, own their state, own their tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule I enforce:&lt;/strong&gt; &lt;em&gt;No MFE imports from another MFE.&lt;/em&gt; Communication only through the event bus or URL. If two MFEs need to share logic, that logic belongs in the design system or a shared utility package — not in either MFE.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is Micro-Frontend Right for Your Team?
&lt;/h2&gt;

&lt;p&gt;Micro-frontends introduce real operational overhead. Be honest about whether you need them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 3 frontend engineers&lt;/td&gt;
&lt;td&gt;Monolith. Micro-frontends will slow you down.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3–8 engineers, one codebase&lt;/td&gt;
&lt;td&gt;Modular monolith with clear domain folders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple teams, frequent merge conflicts&lt;/td&gt;
&lt;td&gt;Strong candidate for MFEs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teams need independent deploy cadences&lt;/td&gt;
&lt;td&gt;MFEs are worth the investment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different tech stacks per team&lt;/td&gt;
&lt;td&gt;MFEs (iframes or Module Federation)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest truth: most teams that migrate to micro-frontends are solving a &lt;em&gt;people and process&lt;/em&gt; problem with a &lt;em&gt;technology&lt;/em&gt; solution. If your teams can agree on shared standards and communicate well, a well-structured monolith might serve you better for longer than you think.&lt;/p&gt;

&lt;p&gt;But when you genuinely need independent deployability and team autonomy at scale? Module Federation is mature enough, the tooling has caught up, and the patterns are now well-understood.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;Looking back at those two migrations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'd define the event bus contract first&lt;/strong&gt;, before writing a single component. The communication layer between MFEs is your most critical API. Treat it like one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'd version the design system from day one&lt;/strong&gt;, not after three teams have forked the button component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'd set bundle size budgets in CI on day one&lt;/strong&gt; and make them fail the build. "We'll optimize later" is a lie we all tell ourselves.&lt;/p&gt;

&lt;p&gt;And honestly? &lt;strong&gt;I'd question whether we needed it at all&lt;/strong&gt; — at least one of those migrations happened because it was architecturally fashionable, not because the team had actually outgrown the monolith. The best architecture is the simplest one that solves your actual problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Micro-frontend architecture is powerful when applied to the right problems: large teams, independent deployments, domain ownership at scale. Module Federation has made the integration story dramatically better than it was even two years ago.&lt;/p&gt;

&lt;p&gt;But it's not free. Shared dependencies, state isolation, design consistency, and integration testing all require intentional effort that a monolith handles for you by default.&lt;/p&gt;

&lt;p&gt;My three-sentence summary:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use Module Federation for run-time integration. Enforce singleton shared dependencies. Never let MFEs import from each other — the event bus is your friend.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're in the middle of a micro-frontend migration — or trying to decide whether to start one — drop a comment. I've been in the trenches on this and I'm happy to dig into specifics.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about frontend architecture, performance, and the messy realities of building large-scale UIs. Follow me on DEV if that's your kind of thing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>architecture</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Thought I Knew React. Then I Watched It Re-render 47 Times on a Button Click.</title>
      <dc:creator>Shudhanshu Raj</dc:creator>
      <pubDate>Tue, 12 May 2026 17:58:52 +0000</pubDate>
      <link>https://forem.com/shudhanshuraj/i-thought-i-knew-react-then-i-watched-it-re-render-47-times-on-a-button-click-4m09</link>
      <guid>https://forem.com/shudhanshuraj/i-thought-i-knew-react-then-i-watched-it-re-render-47-times-on-a-button-click-4m09</guid>
      <description>&lt;p&gt;&lt;em&gt;A story about chasing unnecessary renders, the tools that exposed them, and the patterns that finally made them stop.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Our app had a search bar. It worked fine. It felt fine. Until a designer on the team — curious, with DevTools open — asked why the sidebar, the header, the notification badge, and a completely unrelated recommendations panel all lit up in the React DevTools highlighter every time she typed a single character into it.&lt;/p&gt;

&lt;p&gt;She wasn't filing a bug. She was just confused. I was too, until I sat down and traced it.&lt;/p&gt;

&lt;p&gt;That was the beginning of three weeks I now think of as re-render hell. Not because the app was broken — it wasn't. Performance was "fine." But once you see it, you can't unsee it: a 400-component tree thrashing on every keystroke, most of it doing absolutely nothing meaningful, all of it eating time on lower-end devices that our users in Tier-2 cities actually used.&lt;/p&gt;

&lt;p&gt;What follows is the short version of what we found, what caused it, and — more importantly — what actually fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  First: how to see what's actually happening
&lt;/h2&gt;

&lt;p&gt;You can't fix what you can't see. Before you change a single line of code, turn on the React DevTools profiler and enable &lt;strong&gt;"Highlight updates when components render."&lt;/strong&gt; It's the flame icon in the Components panel. Every component that re-renders flashes a colored border. Watch it while you interact with your app.&lt;/p&gt;

&lt;p&gt;What you're looking for: components lighting up that have no business lighting up. A sidebar re-rendering when you type in a search box. A header re-rendering when you toggle a modal. A card component re-rendering when a completely different card updates.&lt;/p&gt;

&lt;p&gt;In our case, the React DevTools highlighter looked like a Christmas tree. Almost everything was re-rendering on almost every interaction.&lt;/p&gt;

&lt;p&gt;The next tool in the chain is &lt;code&gt;why-did-you-render&lt;/code&gt; — a small library that patches React and logs to the console exactly &lt;em&gt;why&lt;/em&gt; a component re-rendered: which props changed, which state changed, whether it was a context update. Install it once in development, point it at the components you're suspicious of, and let it tell you the truth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wdyr.js — import this at the top of your index.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;whyDidYouRender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@welldone-software/why-did-you-render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;whyDidYouRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;trackAllPureComponents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the console showed us was humbling. Components re-rendering with &lt;code&gt;prevProps === nextProps&lt;/code&gt;. Components re-rendering because a context value object was recreated every render, even though the underlying data hadn't changed. Components re-rendering because a callback was being defined inline and failing referential equality on every pass.&lt;/p&gt;

&lt;p&gt;The problem wasn't one thing. It was a category of thing: &lt;strong&gt;we were creating new references constantly, and React was treating them as new values.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The root cause nobody talks about plainly
&lt;/h2&gt;

&lt;p&gt;React re-renders a component when its state changes, its parent re-renders, or a context it subscribes to updates. That last one is the silent killer, and it's almost always the same underlying mistake.&lt;/p&gt;

&lt;p&gt;Here's the pattern we had everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AuthContext.jsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPermissions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AuthContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine. The problem: every time &lt;code&gt;AuthProvider&lt;/code&gt; re-renders — for any reason — it creates a new &lt;code&gt;value&lt;/code&gt; object. New object reference = React assumes the context changed = every consumer re-renders. Every. Single. One.&lt;/p&gt;

&lt;p&gt;On our app, &lt;code&gt;AuthContext&lt;/code&gt; was consumed in 34 components. Every top-level state change anywhere near the provider triggered 34 re-renders across the tree, most of them pointless.&lt;/p&gt;

&lt;p&gt;The fix was two things used together: &lt;code&gt;useMemo&lt;/code&gt; to stabilize the value object, and — more importantly — &lt;strong&gt;splitting the context.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix that helped most: split your contexts
&lt;/h2&gt;

&lt;p&gt;The most impactful architectural change we made was splitting monolithic contexts into smaller, focused ones. Instead of one &lt;code&gt;AuthContext&lt;/code&gt; that held user data, permissions, and setters, we split it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Separate stable data from volatile data&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// rarely changes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PermissionsContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;  &lt;span class="c1"&gt;// changes on role updates&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AuthActionsContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// never changes (stable callbacks)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPermissions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt; &lt;span class="c1"&gt;// stable forever&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthActionsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PermissionsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PermissionsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;UserContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AuthActionsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a component that only needs &lt;code&gt;setUser&lt;/code&gt; subscribes to &lt;code&gt;AuthActionsContext&lt;/code&gt; and never re-renders when the user object updates. A component that reads &lt;code&gt;user&lt;/code&gt; doesn't re-render when permissions change. Surgical updates instead of broadcast updates.&lt;/p&gt;

&lt;p&gt;We applied this pattern to every context in the app. The re-render count on a typical interaction dropped by roughly 60% overnight. No logic changed. No UX changed. Just topology.&lt;/p&gt;




&lt;h2&gt;
  
  
  memo, useMemo, useCallback — and when they're actually worth it
&lt;/h2&gt;

&lt;p&gt;After the context work, we started reaching for &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt; more deliberately. The keyword there is &lt;em&gt;deliberately&lt;/em&gt;. I've seen codebases where every component is wrapped in &lt;code&gt;memo&lt;/code&gt; and every function is wrapped in &lt;code&gt;useCallback&lt;/code&gt; as a cargo-cult reflex. That's not optimization — that's noise. Each of these has a cost (the comparison itself, the closure overhead), and if the component is cheap to render or re-renders rarely anyway, you're paying a cost for no benefit.&lt;/p&gt;

&lt;p&gt;The mental model that helped us decide when to apply them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;React.memo&lt;/code&gt;&lt;/strong&gt; is worth it when a component is expensive to render &lt;em&gt;and&lt;/em&gt; its parent re-renders frequently &lt;em&gt;and&lt;/em&gt; its props are often the same between those re-renders. A list item in a 200-item list, for example. A sidebar that re-renders whenever any top-level state changes but whose own props almost never change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProductCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onAddToCart&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Expensive render — reads from a selector, renders a complex layout&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;useCallback&lt;/code&gt;&lt;/strong&gt; is worth it specifically when you're passing a function as a prop to a memoized child. Without it, a new function reference on every parent render defeats the &lt;code&gt;memo&lt;/code&gt; entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Without this, ProductCard's memo is useless — new function ref every render&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleAddToCart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ADD_TO_CART&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;useMemo&lt;/code&gt;&lt;/strong&gt; is worth it for expensive computations that are called during render — filtering a large list, computing derived state, building a complex data structure. It is &lt;em&gt;not&lt;/em&gt; worth it for simple object construction unless that object is a context value or a prop passed to a memoized child.&lt;/p&gt;

&lt;p&gt;The question I ask before adding any of these: "What re-renders am I preventing, and is that render actually expensive?" If I can't answer both parts, I don't add the wrapper.&lt;/p&gt;




&lt;h2&gt;
  
  
  The inline function trap
&lt;/h2&gt;

&lt;p&gt;This one is small but it's everywhere and it compounds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This creates a new function on every render of ParentList&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Item&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;Item&lt;/code&gt; is memoized, this defeats the memo. Every render of &lt;code&gt;ParentList&lt;/code&gt; produces a new arrow function, a new prop reference, and a re-render of every &lt;code&gt;Item&lt;/code&gt; in the list.&lt;/p&gt;

&lt;p&gt;The fix is either &lt;code&gt;useCallback&lt;/code&gt; with a stable identity, or — often cleaner — passing the handler and the id separately and letting the child call &lt;code&gt;onSelect(id)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSelect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setSelected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Item&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSelect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;handleSelect&lt;/code&gt; is the same reference across renders. &lt;code&gt;Item&lt;/code&gt; doesn't re-render unless its &lt;code&gt;id&lt;/code&gt; or &lt;code&gt;onSelect&lt;/code&gt; actually changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The monitoring habit that caught regressions
&lt;/h2&gt;

&lt;p&gt;We learned the hard way that performance work without monitoring is just gardening — you trim things back and they grow again. After three weeks of cleanup, we added two habits:&lt;/p&gt;

&lt;p&gt;First, we kept &lt;code&gt;why-did-you-render&lt;/code&gt; wired up in our dev environment, permanently. Any PR that causes a suspicious re-render shows up in the console during review. It became a lightweight CI check without any tooling overhead.&lt;/p&gt;

&lt;p&gt;Second, we added React DevTools profiling to our pre-release checklist for any feature that touched shared state or context. The rule: record a profiling session of the core interaction, look at the flame chart, flag any component that renders more than twice for the same user action. It takes five minutes. It's caught three regressions in the months since.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd tell past-me at the start of this
&lt;/h2&gt;

&lt;p&gt;Don't start with &lt;code&gt;memo&lt;/code&gt; and &lt;code&gt;useCallback&lt;/code&gt;. Start with the DevTools highlighter and &lt;code&gt;why-did-you-render&lt;/code&gt;, because the real answer is almost always upstream — a context that's too wide, a value object that's recreated too often, a state that's placed too high in the tree. Wrapping symptoms in &lt;code&gt;memo&lt;/code&gt; is like putting a rug over a leak. It hides the puddle. It doesn't fix the pipe.&lt;/p&gt;

&lt;p&gt;The other thing: re-render problems are architecture problems in slow motion. They usually trace back to decisions made early — where state lives, how context is structured, what gets colocated — and they compound as the app grows. The earlier you take them seriously, the cheaper they are to fix.&lt;/p&gt;

&lt;p&gt;We have a standing rule now: any new context gets reviewed for how many components will consume it and whether it can be split. It takes ten minutes in a PR. It's saved us weeks of profiling work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? A reaction helps other developers find it. I write about React, frontend architecture, and the unglamorous parts of shipping software at scale.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>performance</category>
      <category>frontend</category>
    </item>
    <item>
      <title>I Spent Six Months Chasing Core Web Vitals. Here’s What Actually Moved the Needle.</title>
      <dc:creator>Shudhanshu Raj</dc:creator>
      <pubDate>Tue, 28 Apr 2026 04:30:00 +0000</pubDate>
      <link>https://forem.com/shudhanshuraj/i-spent-six-months-chasing-core-web-vitals-heres-what-actually-moved-the-needle-2ofd</link>
      <guid>https://forem.com/shudhanshuraj/i-spent-six-months-chasing-core-web-vitals-heres-what-actually-moved-the-needle-2ofd</guid>
      <description>&lt;p&gt;&lt;em&gt;A field guide to LCP, INP, and CLS that skips the theory and gets to what breaks in production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Our dashboard said green. Our users said otherwise.&lt;/p&gt;

&lt;p&gt;For the better part of a year, we shipped features, ran Lighthouse locally, watched scores float between 92 and 98, and patted ourselves on the back. Then one Monday morning, support pinged us: a product manager testing on her own phone swore the listing page felt "stuck" for a second every time she tapped a filter. She wasn't wrong. She just wasn't in our lab.&lt;/p&gt;

&lt;p&gt;That was the moment I learned the most important thing about Core Web Vitals: &lt;strong&gt;the number in your terminal is not the number Google cares about.&lt;/strong&gt; Google cares about what happens to real users, on real devices, on real networks — the 75th percentile of them — and it measures that over a rolling 28 days through the Chrome User Experience Report (CrUX). Your Lighthouse run is a simulation. CrUX is the scoreboard.&lt;/p&gt;

&lt;p&gt;That distinction — lab versus field — reshaped how our team approached performance. What follows is the short version of six months of work, minus the dead ends, focused on the three metrics that matter in 2026: &lt;strong&gt;LCP, INP, and CLS.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The metric most teams are losing on
&lt;/h2&gt;

&lt;p&gt;Let me start with the one that hurts: INP. Interaction to Next Paint replaced First Input Delay in March 2024, and unlike FID — which only measured the delay &lt;em&gt;before&lt;/em&gt; the browser started processing your click — INP measures the full round trip. Click to visual update. Every interaction on the page, not just the first one. The reported score is essentially your worst case.&lt;/p&gt;

&lt;p&gt;Roughly 43% of sites currently fail the 200 ms "good" threshold, and on most of the apps I've touched, it's the metric that takes the deepest rework to fix. LCP is a logistics problem (ship bytes faster). CLS is a discipline problem (reserve space). INP is an architecture problem.&lt;/p&gt;

&lt;p&gt;Here's where it bit us hardest: filters on a product listing page. Each tap triggered a &lt;code&gt;setState&lt;/code&gt; that ran through a context provider, re-rendered about 40 components, recomputed a sort, and then — only then — painted the new chip as "selected." On a mid-range Android over 4G, that round trip hit 480 ms. The user felt it. Chrome reported it. No amount of bundle trimming was going to fix a long task.&lt;/p&gt;

&lt;p&gt;What actually fixed it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Break the task, then yield.&lt;/strong&gt; The single highest-impact change was introducing yield points using &lt;code&gt;scheduler.yield()&lt;/code&gt; (with a fallback to a &lt;code&gt;setTimeout(0)&lt;/code&gt; shim for browsers without it). We split the interaction into "visual feedback first, everything else second."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleFilterTap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Paint the selected state immediately&lt;/span&gt;
  &lt;span class="nf"&gt;setSelectedFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;yieldToMain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Then do the expensive work&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;yieldToMain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;yieldToMain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scheduler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yield&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The perceived responsiveness change was dramatic. The filter chip highlighted instantly. The list updated a tick later. INP at p75 dropped from 480 ms to 170 ms on the same page, with no change to the actual filtering logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Defer non-critical renders with &lt;code&gt;useDeferredValue&lt;/code&gt;.&lt;/strong&gt; In React, marking the filtered results as a deferred value let the input stay responsive while the heavy list re-rendered in the background. Free win, about a week to roll out safely across the product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Kill the long tasks you don't know you're running.&lt;/strong&gt; This one is humbling. Open the Performance panel in Chrome DevTools, record an interaction, and look for any task over 50 ms. In our case, a third-party analytics script was running a synchronous JSON serialization on every click. We had no idea. We moved the script to a web worker. INP dropped another 40 ms on pages where that analytics event fired.&lt;/p&gt;

&lt;p&gt;The pattern that emerged: &lt;strong&gt;INP rewards event handlers that do almost nothing synchronously.&lt;/strong&gt; Anything expensive — filtering, sorting, logging, heavy computations — gets yielded, deferred, or offloaded. If you take one thing from this post, take that.&lt;/p&gt;




&lt;h2&gt;
  
  
  LCP: it's almost always the hero image
&lt;/h2&gt;

&lt;p&gt;Every team I've worked with has the same story with LCP. Someone optimized "the images." LCP was still 3.2 seconds. Because "the images" is not the fix — &lt;em&gt;the LCP element&lt;/em&gt; is the fix, and the LCP element is almost always one specific image above the fold.&lt;/p&gt;

&lt;p&gt;Before you touch anything, identify what the LCP element actually is. You can read it straight from the &lt;code&gt;web-vitals&lt;/code&gt; library in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onLCP&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;web-vitals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;onLCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LCP element:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LCP value:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ninety percent of the time, it's a hero image. Once you know that, the playbook is short and boring, which is the highest compliment I can give a performance playbook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Preload it.&lt;/strong&gt; &lt;code&gt;&amp;lt;link rel="preload" as="image" href="..." fetchpriority="high"&amp;gt;&lt;/code&gt; in the document head. This one line moved our LCP from 2.8 s to 2.1 s on the homepage. Don't preload everything — just the LCP resource. Preload abuse is its own problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;fetchpriority="high"&lt;/code&gt;&lt;/strong&gt; on the image tag itself. Browsers are conservative about image priority by default; you're telling it this one matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use modern formats.&lt;/strong&gt; AVIF first, WebP fallback, JPEG as a last resort. The file size difference between JPEG and AVIF at equivalent quality is routinely 40 to 60 percent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serve the right size.&lt;/strong&gt; A responsive &lt;code&gt;srcset&lt;/code&gt; is not optional. Shipping a 2000-pixel-wide image to a phone that displays it at 400 is the most common unforced error in frontend performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not lazy-load the LCP image.&lt;/strong&gt; I've seen this mistake on production sites shipping in 2026. &lt;code&gt;loading="lazy"&lt;/code&gt; on the hero image is a guaranteed LCP regression.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The harder part of LCP is when the element isn't an image — when it's a block of text that depends on a custom font, for example. In that case, &lt;code&gt;font-display: swap&lt;/code&gt; and preloading the font file are your friends. Accept the brief flash of fallback type. Your users won't notice. Your 75th-percentile LCP will.&lt;/p&gt;




&lt;h2&gt;
  
  
  CLS: the metric that makes you look unprofessional
&lt;/h2&gt;

&lt;p&gt;CLS is the cheapest to fix and the one that makes your site feel the most amateur when you don't. When buttons jump out from under thumbs and ads push content down after the user has started reading, people lose trust in the interface, even if they can't articulate why.&lt;/p&gt;

&lt;p&gt;Three rules. That's all. I have not found a CLS problem in the last three years that wasn't covered by these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every image, video, and iframe gets explicit width and height attributes.&lt;/strong&gt; Even if you're styling them with CSS, the HTML attributes let the browser reserve space before the asset loads. Aspect-ratio boxes work too, but the width/height attributes are simpler and work everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserve space for anything injected late.&lt;/strong&gt; Ad slots, cookie banners, personalization widgets — any element that arrives after first paint needs a height reserved upfront. A &lt;code&gt;min-height&lt;/code&gt; on the container is usually enough. If the slot stays empty, leave it empty. The shift is worse than the blank space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;font-display: swap&lt;/code&gt; with care.&lt;/strong&gt; Swap prevents invisible text, but it can cause a layout shift if your fallback font has significantly different metrics. The &lt;code&gt;size-adjust&lt;/code&gt; descriptor on &lt;code&gt;@font-face&lt;/code&gt; lets you match fallback metrics to your web font and eliminates that shift entirely. This is underused. Most teams haven't heard of it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. CLS under 0.05 is achievable on nearly any site if you follow those three rules. Ours runs at 0.02.&lt;/p&gt;




&lt;h2&gt;
  
  
  The monitoring setup that actually helped
&lt;/h2&gt;

&lt;p&gt;You cannot fix what you cannot see, and DevTools on your machine is not "seeing." We installed the &lt;code&gt;web-vitals&lt;/code&gt; npm package, shipped real-user metrics to our analytics pipeline, and — this was the unlock — &lt;strong&gt;sliced the data by route, device class, and country.&lt;/strong&gt; A single aggregate INP number hides everything. The same site can have great INP for desktop users in Germany and awful INP for mobile users in Brazil, and the aggregate will look mid. Slicing is how you find the real fire.&lt;/p&gt;

&lt;p&gt;We also set alerts at 80% of Google's thresholds — INP at 160 ms, LCP at 2.0 s, CLS at 0.08 — so we'd see regressions before they started eating our CrUX window. A deploy that bumps INP from 150 to 190 still reports "good," but three of those deploys in a month and you're in trouble.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd tell past-me on day one
&lt;/h2&gt;

&lt;p&gt;Three things.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;stop optimizing for Lighthouse.&lt;/strong&gt; Use it as a diagnostic tool, not a scoreboard. The scoreboard lives at CrUX, and the gap between the two can be an order of magnitude. We spent weeks chasing a 98 when we needed to spend a day fixing a filter handler.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;fix the metric that's actually failing, not the one you have opinions about.&lt;/strong&gt; I like LCP. I find it tractable and satisfying. For three weeks I optimized LCP while INP silently tanked. Look at your CrUX dashboard, find the worst of the three, and start there. Then the next worst. No hero shots.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;performance is a product feature, not a cleanup task.&lt;/strong&gt; Every team I've seen succeed at Core Web Vitals treated them like any other product metric: someone owned them, they were reviewed weekly, regressions were treated as bugs, and they were part of the definition of done for new features. Every team I've seen fail at Core Web Vitals treated them as something to "get to after the next launch."&lt;/p&gt;

&lt;p&gt;You know which team gets to the next launch faster.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this was useful, a clap helps other engineers find it. I write about frontend architecture, performance, and the unglamorous parts of shipping software.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>javascript</category>
      <category>react</category>
    </item>
  </channel>
</rss>
