<?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>DEV Community: Shudhanshu Raj</title>
    <description>The latest articles on DEV Community by Shudhanshu Raj (@shudhanshuraj).</description>
    <link>https://dev.to/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>DEV Community: Shudhanshu Raj</title>
      <link>https://dev.to/shudhanshuraj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shudhanshuraj"/>
    <language>en</language>
    <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://dev.to/shudhanshuraj/i-thought-i-knew-react-then-i-watched-it-re-render-47-times-on-a-button-click-4m09</link>
      <guid>https://dev.to/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://dev.to/shudhanshuraj/i-spent-six-months-chasing-core-web-vitals-heres-what-actually-moved-the-needle-2ofd</link>
      <guid>https://dev.to/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>
