<?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: Utkarsh Bansal</title>
    <description>The latest articles on DEV Community by Utkarsh Bansal (@utkarshbansal01).</description>
    <link>https://dev.to/utkarshbansal01</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1663345%2F3c1e05b6-b482-4760-aabe-67fa35a17b9e.jpeg</url>
      <title>DEV Community: Utkarsh Bansal</title>
      <link>https://dev.to/utkarshbansal01</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/utkarshbansal01"/>
    <language>en</language>
    <item>
      <title>The 3-Second Rule of Web Performance — And How to Beat It</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Fri, 03 Jul 2026 14:06:00 +0000</pubDate>
      <link>https://dev.to/utkarshbansal01/the-3-second-rule-of-web-performance-and-how-to-beat-it-1he1</link>
      <guid>https://dev.to/utkarshbansal01/the-3-second-rule-of-web-performance-and-how-to-beat-it-1he1</guid>
      <description>&lt;p&gt;Hello there 👋&lt;/p&gt;

&lt;p&gt;So... I may have gone a little overboard with this one.&lt;/p&gt;

&lt;p&gt;Last time we pulled apart &lt;a href="https://dev.to/utkarshbansal01/how-eslint-actually-works-the-quality-gate-behind-modern-javascript-50nk"&gt;how ESLint actually works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This time, we're dismantling &lt;strong&gt;web performance&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Because it's one of the highest-ROI frontend skills you can learn—and one of the few that can't simply be outsourced to a tool.&lt;/p&gt;

&lt;p&gt;Fortunately, browsers already hand you the diagnosis.&lt;/p&gt;

&lt;p&gt;By the end of this article, you'll know how to read it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Could this have been a 10-part series?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Absolutely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Did I make it one?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Curious what that 10-part series would've looked like?&lt;/strong&gt;&lt;br&gt;
Here you go.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core Web Vitals: LCP, INP, CLS&lt;/li&gt;
&lt;li&gt;The Critical Rendering Path&lt;/li&gt;
&lt;li&gt;JavaScript costs more than you think&lt;/li&gt;
&lt;li&gt;CSS is not free either&lt;/li&gt;
&lt;li&gt;Images&lt;/li&gt;
&lt;li&gt;Fonts&lt;/li&gt;
&lt;li&gt;The network&lt;/li&gt;
&lt;li&gt;Framework patterns (React/Vue)&lt;/li&gt;
&lt;li&gt;Perceived performance&lt;/li&gt;
&lt;li&gt;Lighthouse and field data — measuring what matters&lt;/li&gt;
&lt;li&gt;The performance budget&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The checklist&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, the choice is yours — complete it in one sitting, go section by section, or skip straight to the checklist. No judgment.&lt;/p&gt;


&lt;h3&gt;
  
  
  3 seconds.
&lt;/h3&gt;

&lt;p&gt;That's all you have before visitors start leaving.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sounds harsh?&lt;/strong&gt; Reality is even harsher.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;53% of mobile users abandon a site that takes longer than 3 seconds to load.&lt;/strong&gt; — Google's 2016 research&lt;/p&gt;

&lt;p&gt;But you don't need a study to believe it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How many times have &lt;strong&gt;you&lt;/strong&gt; closed a tab because it took just a little too long?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your users do exactly the same thing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What does that mean?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That portfolio you spent weeks perfecting.&lt;/p&gt;

&lt;p&gt;That product page you pulled three all-nighters building.&lt;/p&gt;

&lt;p&gt;People might never see it.&lt;/p&gt;

&lt;p&gt;Not because it's bad. Because it was slow.&lt;/p&gt;

&lt;p&gt;Google found that improving mobile load times by just &lt;strong&gt;0.1 seconds&lt;/strong&gt; increased retail conversions by &lt;strong&gt;8.4%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's not a rounding error.&lt;br&gt;
That's revenue.&lt;/p&gt;

&lt;p&gt;And that's only half the story.&lt;/p&gt;

&lt;p&gt;Google also uses &lt;strong&gt;Core Web Vitals&lt;/strong&gt; as a search ranking signal.&lt;/p&gt;

&lt;p&gt;That means poor performance doesn't just cost you users — it also costs you visibility.&lt;/p&gt;

&lt;p&gt;Performance isn't just a UX metric. It's a &lt;strong&gt;business metric&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Fast Isn't a Feeling. It's Measurable. &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Everyone says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Make the site faster."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But what does &lt;strong&gt;"fast"&lt;/strong&gt; actually mean?&lt;br&gt;
For the browser, this comes down to three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Did the user see something?&lt;/strong&gt; — Loading performance -&amp;gt; &lt;strong&gt;LCP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did the page respond when I click?&lt;/strong&gt; — Interaction performance -&amp;gt; &lt;strong&gt;INP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did the layout stay stable?&lt;/strong&gt; — Layout stability -&amp;gt; &lt;strong&gt;CLS&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Google spent years figuring out how to measure what users actually experience.&lt;/p&gt;

&lt;p&gt;The result? &lt;strong&gt;Core Web Vitals&lt;/strong&gt;.&lt;br&gt;
Before we go any further...&lt;br&gt;
Let's measure this page together.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Open DevTools (&lt;code&gt;Cmd+Option+I&lt;/code&gt; / &lt;code&gt;Ctrl+Shift+I&lt;/code&gt;) -&amp;gt; Performance&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Keep this tab open. We'll keep coming back to it throughout the article.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Chrome DevTools Performance panel — &lt;strong&gt;Live Metrics view&lt;/strong&gt; (see your scores)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbi6htm9sjz8e7gnyrto.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbi6htm9sjz8e7gnyrto.png" alt="DevTools Live Metrics view" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Chrome DevTools Performance panel — &lt;strong&gt;Record and Reload&lt;/strong&gt; (see why)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffb2z3y2njtnylga5bn5y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffb2z3y2njtnylga5bn5y.png" alt="DevTools Record and Reload" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Spend a minute exploring.&lt;br&gt;
I'll wait.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Performance isn't something you memorize. It's something you experience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These two views tell you &lt;strong&gt;what's slow, why it's slow, and which Core Web Vital is paying the price.&lt;/strong&gt;&lt;br&gt;
That's &lt;strong&gt;90% of performance engineering.&lt;/strong&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  LCP, INP, CLS — What They Actually Measure
&lt;/h3&gt;

&lt;p&gt;Here's what my Live Metrics view showed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LCP: 0.18s&lt;/li&gt;
&lt;li&gt;CLS: 0.00&lt;/li&gt;
&lt;li&gt;INP: 48ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me explain what these mean.&lt;/p&gt;
&lt;h4&gt;
  
  
  LCP — Largest Contentful Paint
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;When does the user finally see the main content?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not the spinner. Not the navigation.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;largest visible element in the viewport&lt;/strong&gt; — because that's usually why they came.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Good       -&amp;gt; under 2.5 s
Needs work -&amp;gt; 2.5 s – 4 s
Poor       -&amp;gt; over 4 s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  INP — Interaction to Next Paint
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;How quickly does the page react when someone clicks, taps, or types?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not scrolling. Not hovering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Good       -&amp;gt; under 200 ms
Needs work -&amp;gt; 200 ms – 500 ms
Poor       -&amp;gt; over 500 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under 200ms, &lt;strong&gt;the interaction feels instant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Past 500ms, people start wondering if the click even worked.&lt;/p&gt;

&lt;h4&gt;
  
  
  CLS — Cumulative Layout Shift
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;How much does the page unexpectedly move while someone is using it?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ever clicked a button just as the page shifted?&lt;br&gt;
You've experienced CLS.&lt;/p&gt;

&lt;p&gt;If a late-loading banner affects &lt;strong&gt;50%&lt;/strong&gt; of the viewport and pushes content &lt;strong&gt;20%&lt;/strong&gt; of the viewport height:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.5 (impact) × 0.2 (distance) = 0.1 shift score
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every unexpected layout shift &lt;strong&gt;adds to your CLS score&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Good       -&amp;gt; under 0.1
Needs work -&amp;gt; 0.1 – 0.25
Poor       -&amp;gt; over 0.25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  All Three, In One Session
&lt;/h3&gt;

&lt;p&gt;These metrics don't happen in isolation.&lt;/p&gt;

&lt;p&gt;They happen on the same page. To the same user. Often within a few seconds.&lt;/p&gt;

&lt;p&gt;Watch it play out:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/utkarshbansal01/embed/MYJozLe?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  0ms    -&amp;gt; Browser sends request
 300ms   -&amp;gt; HTML received, parser starts building the DOM
 600ms   -&amp;gt; Browser discovers render-blocking CSS file.
            Parsing pauses.
            Nothing is painted.
            User sees a blank screen.
 900ms   -&amp;gt; CSS loads. Parsing resumes.
1200ms   -&amp;gt; Hero image starts downloading.
2400ms   -&amp;gt; Hero image finishes rendering.
            ↑ LCP
2600ms   -&amp;gt; User taps the menu.
2601ms   -&amp;gt; A 400ms JavaScript task is already running.
            Browser can't respond yet.
3000ms   -&amp;gt; JS finishes. The click is finally processed.
            ↑ INP
3100ms   -&amp;gt; Menu opens. User starts reading.
3300ms   -&amp;gt; Font finishes loading.
            Text shifts down.
3500ms   -&amp;gt; An ad appears.
            Content shifts again.
            User taps the wrong button.
            ↑ CLS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three metrics.&lt;br&gt;
One page load.&lt;br&gt;
One user who had a genuinely terrible experience.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Browser Has a Pipeline. Your Resources Keep Interrupting It. &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;In the last section we measured &lt;strong&gt;LCP&lt;/strong&gt;, &lt;strong&gt;INP&lt;/strong&gt;, and &lt;strong&gt;CLS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Those metrics aren't random.&lt;/p&gt;

&lt;p&gt;They all come from the same pipeline the browser uses to load a page.&lt;/p&gt;

&lt;p&gt;Two rules govern that pipeline.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rule #1 — No DOM + CSSOM, no paint.
&lt;/h3&gt;

&lt;p&gt;HTML builds the &lt;strong&gt;DOM&lt;/strong&gt;.&lt;br&gt;
CSS builds the &lt;strong&gt;CSSOM&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The browser needs both before it can paint a single pixel.&lt;/p&gt;

&lt;p&gt;No CSSOM.&lt;br&gt;
No paint.&lt;br&gt;
Just a blank screen.&lt;br&gt;
That's the &lt;strong&gt;Critical Rendering Path&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the whole pipeline, start to finish:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4r8rjoqsrbpj430xbn8m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4r8rjoqsrbpj430xbn8m.png" alt="Critical Rendering Path: HTML and CSS parse into DOM and CSSOM trees, merge into a Render Tree, then flow through Layout, Paint, and Composite" width="800" height="737"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The thing is, you don't have to wait for the entire stylesheet before the first paint happens.&lt;/p&gt;

&lt;p&gt;Only the CSS for what's visible right now actually matters.&lt;/p&gt;

&lt;p&gt;Everything below the fold can wait.&lt;br&gt;
That's &lt;strong&gt;critical CSS&lt;/strong&gt;.&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;!-- 🔴 Browser waits for the whole file before it can paint --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"styles.css"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- 🟢 Critical styles inline. The rest loads without blocking. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;/* just enough CSS for what's visible right away */&lt;/span&gt;
  &lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"styles.css"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"print"&lt;/span&gt; &lt;span class="na"&gt;onload=&lt;/span&gt;&lt;span class="s"&gt;"this.media='all'"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;media="print"&lt;/code&gt; part isn't really about printing.&lt;/p&gt;

&lt;p&gt;It tricks the browser into treating the file as non-blocking.&lt;/p&gt;

&lt;p&gt;Once it finishes loading, &lt;code&gt;onload&lt;/code&gt; flips it back to &lt;code&gt;media="all"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No network round-trip blocking your first paint.&lt;/p&gt;

&lt;p&gt;One more thing: add a &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt; fallback too, in case JavaScript is off.&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;!-- Without JS, the onload above never fires, so this stylesheet would be --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- stuck on media="print" forever. The noscript version gives JS-less --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- browsers a normal, blocking stylesheet instead. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;noscript&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"styles.css"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/noscript&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rule #2 — Without &lt;code&gt;defer&lt;/code&gt; or &lt;code&gt;async&lt;/code&gt;, the parser waits.
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; without &lt;code&gt;defer&lt;/code&gt; or &lt;code&gt;async&lt;/code&gt; is &lt;strong&gt;parser-blocking&lt;/strong&gt; — though what that actually costs you depends on where the tag sits, as you'll see below.&lt;/p&gt;

&lt;p&gt;The browser pauses HTML parsing, executes the script, then resumes where it left off.&lt;/p&gt;

&lt;p&gt;Watch what happens when you load the same script three different ways.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/utkarshbansal01/embed/GgrEzgZ?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here's what it looks like in DevTools:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F28lrlbh3vz3nxzss52da.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F28lrlbh3vz3nxzss52da.png" alt="Main thread flame chart" width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those markers tell the story of the page load.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Marker&lt;/th&gt;
&lt;th&gt;Full name&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;First Contentful Paint&lt;/td&gt;
&lt;td&gt;First visible content appears&lt;/td&gt;
&lt;td&gt;167.66ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DCL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DOMContentLoaded&lt;/td&gt;
&lt;td&gt;HTML finished parsing&lt;/td&gt;
&lt;td&gt;321.39ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;L&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;onLoad&lt;/td&gt;
&lt;td&gt;Initial resources finished loading&lt;/td&gt;
&lt;td&gt;697.21ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Largest Contentful Paint&lt;/td&gt;
&lt;td&gt;Largest visible element rendered&lt;/td&gt;
&lt;td&gt;861.26ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice something.&lt;br&gt;
The browser isn't "&lt;strong&gt;done&lt;/strong&gt;" once.&lt;br&gt;
It's done in stages.&lt;/p&gt;

&lt;p&gt;Timeline comparing script loading:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fma4oqbjlsx57whaiczr2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fma4oqbjlsx57whaiczr2.png" alt="Timeline comparing script loading: no attribute in head, no attribute at end of body, defer, and two async scenarios" width="800" height="678"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same &lt;strong&gt;FCP&lt;/strong&gt;. Very different &lt;strong&gt;LCP&lt;/strong&gt; — often the difference between passing and failing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;defer&lt;/code&gt;&lt;/strong&gt; -&amp;gt; for app code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;async&lt;/code&gt;&lt;/strong&gt; -&amp;gt; for independent scripts like analytics, ads, widgets, API calls, or anything that doesn't depend on the DOM.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  One Exception That Trips Up Almost Everyone
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;async&lt;/code&gt; and &lt;code&gt;defer&lt;/code&gt; only work on &lt;strong&gt;external&lt;/strong&gt; scripts — inline scripts ignore both.&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;!-- ✅ Works --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"app.js"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- ❌ defer ignored — no src --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two good options if you must put an inline script in &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option 1 — Wait for the DOM.
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;document&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of it as the inline equivalent of putting your script at the end of &lt;strong&gt;&lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option 2 — Use modules (deferred automatically):
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the full picture.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Script&lt;/th&gt;
&lt;th&gt;Best placement&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;External, no attribute&lt;/td&gt;
&lt;td&gt;End of &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Works, but starts downloading late&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External + &lt;code&gt;defer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Best choice — parallel download, safe execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External + &lt;code&gt;async&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Independent scripts only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline, no wrapper&lt;/td&gt;
&lt;td&gt;End of &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;async&lt;/code&gt; and &lt;code&gt;defer&lt;/code&gt; are ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline + &lt;code&gt;DOMContentLoaded&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Waits for the DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline + &lt;code&gt;type="module"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deferred automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Then Why Does Everyone Still Teach "Put JavaScript at the Bottom"?
&lt;/h3&gt;

&lt;p&gt;I wondered the same thing until I looked into the history — and found something interesting.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It was the right advice in 2007. It just never got updated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Back then, &lt;code&gt;defer&lt;/code&gt; existed, but browser support was unreliable.&lt;/p&gt;

&lt;p&gt;Internet Explorer interpreted it differently.&lt;br&gt;
Firefox had bugs.&lt;/p&gt;

&lt;p&gt;Putting scripts at the end of &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; was the only reliable solution.&lt;/p&gt;

&lt;p&gt;Later, &lt;em&gt;Steve Souders' 2007 book &lt;strong&gt;High Performance Web Sites&lt;/strong&gt;&lt;/em&gt; popularized the pattern. And the advice spread through books, tutorials, bootcamps, and never really changed.&lt;/p&gt;

&lt;p&gt;Now, every modern browser implements &lt;code&gt;defer&lt;/code&gt; consistently.&lt;/p&gt;

&lt;p&gt;The workaround is no longer necessary.&lt;br&gt;
Let's &lt;strong&gt;leave 2007 behind&lt;/strong&gt;, shall we?&lt;/p&gt;


&lt;h2&gt;
  
  
  JavaScript Costs More Than You Think &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Blocking paint is only half the story.&lt;br&gt;
The other half is JavaScript.&lt;br&gt;
It's more than just a download.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;200KB image&lt;/strong&gt; and a &lt;strong&gt;200KB JavaScript&lt;/strong&gt; file might be the same size.&lt;/p&gt;

&lt;p&gt;They're nowhere near the same cost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Image (200KB):      download -&amp;gt; decode -&amp;gt; done
JavaScript (200KB): download -&amp;gt; parse -&amp;gt; compile -&amp;gt; execute -&amp;gt; (blocks interactions)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Real-world cost?&lt;/strong&gt; Up to &lt;strong&gt;30%&lt;/strong&gt; of a page's total load time can go purely into JavaScript execution.&lt;/p&gt;

&lt;p&gt;Not because kilobytes are evil.&lt;/p&gt;

&lt;p&gt;Because the browser has to process every byte before it can run.&lt;/p&gt;

&lt;p&gt;Optimizing JavaScript boils down to two goals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ship less JavaScript.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run what you ship without blocking the main thread.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Goal #1 — Ship Less JavaScript
&lt;/h3&gt;

&lt;p&gt;The fastest code is the code users never download.&lt;/p&gt;

&lt;h4&gt;
  
  
  Code Splitting — Ship Only What the User Needs
&lt;/h4&gt;

&lt;p&gt;Most apps ship one giant bundle.&lt;/p&gt;

&lt;p&gt;Every route.&lt;br&gt;
Every feature.&lt;br&gt;
Every component.&lt;/p&gt;

&lt;p&gt;Even though most users never touch most of it.&lt;br&gt;
Code splitting fixes that.&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;// Static import — downloads immediately&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;initMap&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;./map.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="c1"&gt;// Dynamic import — downloads only when needed&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-map&lt;/span&gt;&lt;span class="dl"&gt;'&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initMap&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;./map.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;initMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map-container&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="c1"&gt;// Route-based splitting&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&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="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;./pages/dashboard.js&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;/profile&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="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;./pages/profile.js&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;/settings&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="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;./pages/settings.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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard never downloads for the users who never open it.&lt;/p&gt;

&lt;p&gt;That's not optimization.&lt;br&gt;
It's simply not wasting bandwidth.&lt;br&gt;
React, Vue, and Angular all build on the same idea.&lt;br&gt;
Under the hood, they're just using dynamic &lt;code&gt;import()&lt;/code&gt;.&lt;br&gt;
See it yourself.&lt;br&gt;
Open DevTools -&amp;gt; &lt;strong&gt;Network -&amp;gt; JS&lt;/strong&gt; on dev.to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Far6uolc5z5p97mlmpx71.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Far6uolc5z5p97mlmpx71.png" alt="Network tab: 14 JS chunks with content-hash filenames" width="799" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fourteen JavaScript files.&lt;br&gt;
None over 200KB.&lt;br&gt;
Fourteen focused chunks beat one massive bundle.&lt;/p&gt;


&lt;h4&gt;
  
  
  Tree Shaking — Remove What You Don't Use
&lt;/h4&gt;

&lt;p&gt;Install &lt;code&gt;lodash&lt;/code&gt;. Use one function. The whole library ships anyway.&lt;/p&gt;

&lt;p&gt;That's because lodash uses CommonJS — bundlers can't tree-shake it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lodash-es&lt;/code&gt; is the same library, rewritten as ES modules.&lt;/p&gt;

&lt;p&gt;Switch to &lt;code&gt;lodash-es&lt;/code&gt; and use a named import:&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;// 🔴 Ships ALL of lodash (~72KB minified)&lt;/span&gt;
&lt;span class="c1"&gt;// CommonJS — bundlers can't tree-shake this&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&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;lodash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a.b.c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// 🟢 Ships ONLY get (~5KB minified)&lt;/span&gt;
&lt;span class="c1"&gt;// ES modules — bundler strips everything you never imported&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;get&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;lodash-es&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a.b.c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One switch. &lt;strong&gt;72KB -&amp;gt; ~5KB.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Want to find where this is happening in your own project?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx vite-bundle-visualizer  &lt;span class="c"&gt;# see your full bundle as a treemap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bundle analysis almost always reveals something that doesn't belong.&lt;/p&gt;

&lt;h4&gt;
  
  
  Choose Smaller Libraries
&lt;/h4&gt;

&lt;p&gt;The fix isn't always how you import — it's what you import.&lt;/p&gt;

&lt;p&gt;Open &lt;strong&gt;bundlephobia.com&lt;/strong&gt;.&lt;br&gt;
Search &lt;strong&gt;moment&lt;/strong&gt;. Then search &lt;strong&gt;date-fns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0e91apuegyp0fux5etfy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0e91apuegyp0fux5etfy.png" alt="moment vs date-fns bundle size comparison on bundlephobia" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same job. Very different cost.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Moment:&lt;/strong&gt; 75.4KB minified&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;date-fns:&lt;/strong&gt; 17.1KB minified&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's roughly &lt;strong&gt;1.5s vs 342ms&lt;/strong&gt; on Slow 3G.&lt;/p&gt;

&lt;p&gt;Moment became so large that its own maintainers recommend not using it for new projects.&lt;/p&gt;

&lt;p&gt;Not because it's broken. Because it no longer makes sense.&lt;/p&gt;


&lt;h3&gt;
  
  
  Goal #2 — Keep the Main Thread Free
&lt;/h3&gt;

&lt;p&gt;Shipping less JavaScript gets it to the browser faster.&lt;/p&gt;

&lt;p&gt;But whatever you ship still has to execute &lt;strong&gt;on the main thread&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Keep it free.&lt;/p&gt;


&lt;h4&gt;
  
  
  Long Tasks — Stop Freezing the Page
&lt;/h4&gt;

&lt;p&gt;The browser can only do one thing at a time.&lt;/p&gt;

&lt;p&gt;If JavaScript keeps the main thread busy for more than &lt;strong&gt;50ms&lt;/strong&gt;, everything else waits.&lt;/p&gt;

&lt;p&gt;Clicks lag.&lt;br&gt;
Scrolling freezes.&lt;br&gt;
Animations drop frames.&lt;br&gt;
That's a &lt;strong&gt;Long Task&lt;/strong&gt;.&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;// 🔴 Blocks the main thread&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processLargeDataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;data&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="nf"&gt;expensiveCalculation&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="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// 🟢 Gives the browser time to breathe&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processLargeDataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;expensiveCalculation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&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;span class="k"&gt;await&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;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;results&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;setTimeout(resolve, 0)&lt;/code&gt; isn't making your code faster.&lt;/p&gt;

&lt;p&gt;It's giving the browser a chance to handle pending input before continuing.&lt;/p&gt;

&lt;p&gt;Feel the difference yourself.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/utkarshbansal01/embed/vEgJaYx?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Now inspect what's happening.&lt;/p&gt;

&lt;p&gt;Open the CodePen -&amp;gt; &lt;strong&gt;Inspect -&amp;gt; Performance -&amp;gt; Record -&amp;gt; Reload&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Look for the &lt;strong&gt;red diagonal stripe&lt;/strong&gt; in Main thread.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsnxlwp2p0y838pzx82qa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsnxlwp2p0y838pzx82qa.png" alt="Long Task (112ms) flagged in DevTools Performance panel" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every task longer than &lt;strong&gt;50ms&lt;/strong&gt; gets flagged.&lt;br&gt;
The yielding version doesn't.&lt;br&gt;
Breaking work into chunks fixes many long tasks.&lt;br&gt;
Sometimes, though...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The work itself is the problem.&lt;/strong&gt;&lt;/p&gt;


&lt;h4&gt;
  
  
  Web Workers — Move Heavy Work Off the Main Thread
&lt;/h4&gt;

&lt;p&gt;Some work simply doesn't belong on the main thread.&lt;br&gt;
&lt;strong&gt;Things like:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image processing.&lt;/li&gt;
&lt;li&gt;Large datasets.&lt;/li&gt;
&lt;li&gt;Complex calculations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Move it to a Worker.&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;// worker.js — separate thread, never blocks UI&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;heavyCalculation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// main.js — page stays responsive&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&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;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;largeDataset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&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;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;Done:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&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;Here's what that separation looks like — two threads, one message-passing bridge:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8g0u8pm51r56itn73tu3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8g0u8pm51r56itn73tu3.png" alt="Web worker thread model" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/utkarshbansal01/embed/qERPWrw?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Turn the Worker &lt;strong&gt;OFF&lt;/strong&gt;.&lt;br&gt;
Keep pressing &lt;code&gt;+&lt;/code&gt; until the particles begin to stutter.&lt;br&gt;
Turn it back &lt;strong&gt;ON&lt;/strong&gt;.&lt;br&gt;
Same computation.&lt;br&gt;
Different thread.&lt;br&gt;
You can feel it — and the FPS counter backs you up.&lt;/p&gt;

&lt;p&gt;In the demo, the main thread drops from 60fps to near-zero during heavy computation. The Worker keeps it at 60.&lt;/p&gt;

&lt;p&gt;Want proof beyond feel? Open the demo in a new tab, start a Performance recording, and toggle the Worker a few times before you stop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe4bn3a0ec1our6kjq3x8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe4bn3a0ec1our6kjq3x8.png" alt="Step 1 — Performance tab selected, click Record" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fps26su9ft2muh9dwtfrz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fps26su9ft2muh9dwtfrz.png" alt="Step 2 — toggle worker at 6s, 12s, 18s then stop" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flosalkogqzyeediu5lk1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flosalkogqzyeediu5lk1.png" alt="Flame chart: Worker active, Main thread unblocked" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Worker&lt;/strong&gt; track shows up the moment it's switched on, and the Main thread goes quiet right alongside it. Switch it off, and the work lands straight back on the Main thread — the same moment the frame rate and CPU graph start complaining.&lt;/p&gt;

&lt;p&gt;Nothing became faster.&lt;br&gt;
The work simply stopped blocking the UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the entire point of Web Workers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before using one, remember three things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workers can't access the DOM.&lt;/strong&gt; No &lt;code&gt;document&lt;/code&gt;, no &lt;code&gt;querySelector&lt;/code&gt;, no element event listeners. They process data and communicate through &lt;code&gt;postMessage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messaging has a cost.&lt;/strong&gt; Every message crosses a thread boundary. For tiny updates, that overhead can outweigh the benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threads aren't free.&lt;/strong&gt; Workers consume memory and scheduling resources. A few heavy Workers are great. Dozens of tiny ones usually aren't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser will never create a Worker for you.&lt;br&gt;
If you want another thread, you have to ask for one.&lt;/p&gt;


&lt;h4&gt;
  
  
  Passive Listeners — One Word. Instant Win.
&lt;/h4&gt;

&lt;p&gt;Heavy JavaScript isn't the only thing that blocks the main thread.&lt;/p&gt;

&lt;p&gt;Sometimes the browser is waiting...&lt;br&gt;
For &lt;strong&gt;your event listener&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you scroll or touch the screen, the browser has to answer one question first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Is this listener about to call &lt;code&gt;preventDefault()&lt;/code&gt;?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If it might, scrolling has to wait.&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;// 🔴 Browser waits before scrolling&lt;/span&gt;
&lt;span class="nb"&gt;window&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateScrollPosition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// 🟢 Browser scrolls immediately&lt;/span&gt;
&lt;span class="nb"&gt;window&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateScrollPosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;passive&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;passive: true&lt;/code&gt; is a promise:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I won't cancel scrolling."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That lets the browser scroll immediately instead of waiting for JavaScript.&lt;/p&gt;

&lt;p&gt;One word.&lt;br&gt;
Instantly smoother scrolling.&lt;br&gt;
Chrome even warns you when you forget.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Catch it in the act:&lt;/strong&gt; Open the CodePen link -&amp;gt; DevTools -&amp;gt; Console -&amp;gt; interact with the hamburger menu. You'll see the warning. Uncomment the passive version and it disappears.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CodePen link: &lt;a href="https://codepen.io/utkarshbansal01/pen/dPNVbNx" rel="noopener noreferrer"&gt;open it here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07u3desiqc4mm49o8sel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07u3desiqc4mm49o8sel.png" alt="Passive listener violation in DevTools Console" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  The JavaScript You Didn't Write
&lt;/h3&gt;

&lt;p&gt;So far we've been fixing &lt;strong&gt;your&lt;/strong&gt; JavaScript.&lt;br&gt;
Sometimes the slowest JavaScript isn't yours at all.&lt;br&gt;
It's the third-party scripts you embedded.&lt;br&gt;
&lt;strong&gt;Here's the cost:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downloads from a server you don't control.&lt;/li&gt;
&lt;li&gt;Runs on your user's device.&lt;/li&gt;
&lt;li&gt;Competes for the same CPU and main thread your own code needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with how it gets requested in the first place.&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;!-- 🔴 Blocks HTML parsing --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://analytics.example.com/track.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- 🟢 Downloads in parallel --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://analytics.example.com/track.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fixes when it loads.&lt;br&gt;
It doesn't fix what happens once it's there.&lt;br&gt;
Open DevTools.&lt;br&gt;
Go to &lt;strong&gt;More tools -&amp;gt; Coverage&lt;/strong&gt;.&lt;br&gt;
Record a page load.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fij1yam08fl1z0cmakr83.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fij1yam08fl1z0cmakr83.png" alt="Coverage tab: unused JS percentage per script" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That screenshot tells a painful story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;97%&lt;/strong&gt; of one script never executed.&lt;/p&gt;

&lt;p&gt;It still downloaded, parsed, compiled, and competed for CPU time.&lt;/p&gt;

&lt;p&gt;For code that never ran.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;async&lt;/code&gt; only changes when a script shows up. It does nothing about how much of it was dead weight to begin with.&lt;/p&gt;

&lt;p&gt;Every third-party script is someone else's code spending your user's battery, bandwidth, and main-thread time.&lt;/p&gt;

&lt;p&gt;Treat each one like a dependency.&lt;br&gt;
Because that's exactly what it is.&lt;/p&gt;

&lt;p&gt;For the expensive embeds — YouTube, Maps, chat widgets — &lt;code&gt;async&lt;/code&gt; isn't enough.&lt;/p&gt;

&lt;p&gt;Don't load them at all.&lt;br&gt;
Not until &lt;strong&gt;someone actually wants them.&lt;/strong&gt;&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="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"video-facade"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"loadPlayer(this)"&lt;/span&gt; &lt;span class="na"&gt;data-id=&lt;/span&gt;&lt;span class="s"&gt;"dQw4w9WgXcQ"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"thumbnail.webp"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Play video"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"640"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"360"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;▶ Play&lt;span class="nt"&gt;&amp;lt;/button&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 fastest third-party script is the one that never loads.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  CSS Is Not Free Either &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Think you're done?&lt;/p&gt;

&lt;p&gt;JavaScript isn't the only thing the browser has to process.&lt;/p&gt;

&lt;p&gt;You already met CSS once, blocking your first paint back in the Critical Rendering Path.&lt;/p&gt;

&lt;p&gt;That was the loading cost. This one's different.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CSS.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every developer learns CSS as a stylesheet to build beautiful websites.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does the browser see it the same way?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It doesn't.&lt;br&gt;
The browser treats CSS as layout instructions.&lt;/p&gt;

&lt;p&gt;Every time you change an element's size or position, it has to answer one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Did this affect anything else?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes the answer is one element.&lt;br&gt;
Sometimes it's the entire page.&lt;br&gt;
That's a &lt;strong&gt;reflow&lt;/strong&gt;.&lt;br&gt;
Reflows cost you frames.&lt;/p&gt;

&lt;p&gt;The classic mistake looks harmless — say you're padding out a row of cards to match whichever one's tallest:&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;// 🔴 Read, write, read, write — once per card, every card&lt;/span&gt;
&lt;span class="nx"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&lt;/span&gt;
  &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// 🟢 Read everything first, then write everything&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cards&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;card&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;heights&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading layout (&lt;code&gt;offsetHeight&lt;/code&gt;) immediately after writing layout (&lt;code&gt;style.height&lt;/code&gt;) forces the browser to stop and recalculate synchronously.&lt;/p&gt;

&lt;p&gt;Do it inside a loop and you pay that cost every iteration.&lt;/p&gt;

&lt;p&gt;That's called &lt;strong&gt;layout thrashing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's common in card grids, feed lists, accordions — anything where you read layout and write styles in the same loop.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/utkarshbansal01/embed/PwWKjdy?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Two demos to run:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Animation&lt;/strong&gt; — hit Start. Watch the FPS counters. &lt;code&gt;margin-top&lt;/code&gt; drops frames. &lt;code&gt;transform&lt;/code&gt; stays at 60.&lt;br&gt;
&lt;strong&gt;Resize 50 Elements&lt;/strong&gt; — hit ⛔ Run thrashing, then ✅ Run batched. The ms timers show the real cost. Same 50 elements. Same result. Completely different price.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Animate Without Making the Browser Sweat
&lt;/h3&gt;

&lt;p&gt;Want to animate something?&lt;br&gt;
Not every CSS property costs the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;margin-top&lt;/strong&gt; vs &lt;strong&gt;transform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One keeps the browser busy.&lt;br&gt;
The other barely touches it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* 🔴 Layout recalculates every frame */&lt;/span&gt;
&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* 🟢 GPU moves the layer */&lt;/span&gt;
&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-4px&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;Identical visual result.&lt;br&gt;
Completely different cost.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;margin-top&lt;/code&gt; changes layout.&lt;/p&gt;

&lt;p&gt;Every frame, the browser recalculates positions before it can paint.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;transform&lt;/code&gt; skips layout entirely.&lt;/p&gt;

&lt;p&gt;The GPU compositor moves the layer without touching the DOM.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/utkarshbansal01/embed/KwavqGP?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Hit Start.&lt;/strong&gt; The animation looks the same. Watch the CPU meters instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One rule for every animation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;transform&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt;&lt;/strong&gt; are the two safest properties the GPU compositor can animate without touching the main thread.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else triggers layout, paint, or both.&lt;/p&gt;




&lt;h3&gt;
  
  
  CSS Containment — Tell the Browser What Can't Change
&lt;/h3&gt;

&lt;p&gt;Imagine every component had invisible walls around it.&lt;br&gt;
That's exactly what &lt;code&gt;contain&lt;/code&gt; does.&lt;br&gt;
Changes inside stay inside.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* changes inside don't affect outside */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.widget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* fully isolated from the rest of the page */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every contained component becomes an island — when something changes inside it, the browser doesn't have to recheck the rest of the page.&lt;/p&gt;

&lt;p&gt;On a page with hundreds of cards, that's the difference between the browser rechecking one card's layout and rechecking all of them.&lt;/p&gt;

&lt;p&gt;Real-world A/B tests back this up. Applying containment to product tiles on a high-traffic e-commerce category page cut INP by &lt;strong&gt;~120ms on mobile&lt;/strong&gt;. In prototype testing, adding DOM elements on interaction dropped rendering work from &lt;strong&gt;732ms to 54ms&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Skip Rendering What Nobody Can See
&lt;/h3&gt;

&lt;p&gt;Why render content that's still three screens away?&lt;br&gt;
You don't have to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.article-section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;content-visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;contain-intrinsic-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt; &lt;span class="m"&gt;600px&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;content-visibility: auto&lt;/code&gt; tells the browser:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Ignore this until it's about to enter the viewport."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No layout.&lt;br&gt;
No paint.&lt;br&gt;
No compositing.&lt;br&gt;
Not until the user gets there.&lt;/p&gt;

&lt;p&gt;It can cut rendering work by &lt;strong&gt;50% or more&lt;/strong&gt;. One real page dropped from 232ms to 30ms. That's a &lt;strong&gt;7x improvement&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;contain-intrinsic-size&lt;/code&gt; reserves space while the section is skipped, preventing layout shifts when it finally renders.&lt;/p&gt;

&lt;p&gt;Choose a value close to the section's real height.&lt;/p&gt;

&lt;p&gt;Too far off, and the page will jump when the content appears.&lt;/p&gt;

&lt;p&gt;Chrome, Firefox, and Safari all support it today. Anything older simply ignores it and renders everything normally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe to ship.&lt;/strong&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;CSS optimized.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rendering pipeline is doing as little work as possible.&lt;/p&gt;

&lt;p&gt;But there's one resource that's usually bigger than all of it combined.&lt;/p&gt;

&lt;p&gt;Images.&lt;/p&gt;


&lt;h2&gt;
  
  
  Images Are Probably Your Biggest Problem &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Let's prove it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Open DevTools -&amp;gt; Network -&amp;gt; Reload -&amp;gt; All -&amp;gt; sort by Size.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6n4yrolf0snmpot4uxp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6n4yrolf0snmpot4uxp.png" alt="Network tab: images dominate by size" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a blog post. Code blocks. A handful of screenshots.&lt;/p&gt;

&lt;p&gt;Nothing like a portfolio, an e-commerce site, or Instagram.&lt;/p&gt;

&lt;p&gt;And still, images dominate the top of that list.&lt;br&gt;
Now imagine an online store.&lt;br&gt;
Or a travel website.&lt;br&gt;
Or a social media feed.&lt;br&gt;
Odds are the top of that list isn't JavaScript.&lt;br&gt;
It never is.&lt;br&gt;
It's the thing everyone looks at, and nobody optimizes.&lt;/p&gt;



&lt;p&gt;Four decisions. That's all it takes.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Choose the Right Format
&lt;/h3&gt;

&lt;p&gt;Format matters more than compression settings.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;Photos (fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;Transparency (fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;Default choice — 25–34% smaller than JPEG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;Best compression — about 50% smaller than JPEG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG&lt;/td&gt;
&lt;td&gt;Icons, logos, illustrations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let the browser choose the best format it supports.&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="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"hero.avif"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/avif"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"hero.webp"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/webp"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero image"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern browsers get AVIF.&lt;br&gt;
Older ones fall back automatically.&lt;br&gt;
No JavaScript required.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Always Tell the Browser the Size
&lt;/h3&gt;

&lt;p&gt;Always set &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt;. These two attributes fix one of the most common causes of CLS.&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="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.webp"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without them, the browser has no idea how much space to reserve.&lt;/p&gt;

&lt;p&gt;The image loads.&lt;br&gt;
Everything moves.&lt;br&gt;
That's layout shift.&lt;/p&gt;

&lt;p&gt;For your &lt;strong&gt;LCP image&lt;/strong&gt; specifically, add &lt;code&gt;fetchpriority="high"&lt;/code&gt; — it tells the browser: "Download this before less important images."&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="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero.webp"&lt;/span&gt; &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Don't Send a 4K Image to a Phone
&lt;/h3&gt;

&lt;p&gt;A phone doesn't need desktop-sized images.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;srcset&lt;/code&gt; lets the browser choose the right one.&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="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"hero-800.jpg"&lt;/span&gt;
  &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w, hero-2400.jpg 2400w"&lt;/span&gt;
  &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 600px) 100vw,
         (max-width: 1200px) 50vw,
         1200px"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Hero"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A phone downloads the 400px version.&lt;br&gt;
A desktop downloads the larger one.&lt;br&gt;
Same image.&lt;br&gt;
Different file.&lt;br&gt;
Less wasted bandwidth.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Don't Download Images Nobody Can See
&lt;/h3&gt;

&lt;p&gt;Images users can't see yet don't need to load immediately.&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="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"product.webp"&lt;/span&gt; &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"400"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"300"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Product"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One attribute.&lt;/p&gt;

&lt;p&gt;Often a 30–50% cut to initial page weight on an image-heavy page, sometimes more.&lt;/p&gt;

&lt;p&gt;There is one important exception: Never lazy-load your &lt;strong&gt;LCP image&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the hero is above the fold, delaying it delays your LCP.&lt;/p&gt;

&lt;p&gt;Four decisions. One theme.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smaller files.&lt;/li&gt;
&lt;li&gt;Fewer pixels.&lt;/li&gt;
&lt;li&gt;Fewer downloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why images are usually the biggest performance win.&lt;/p&gt;




&lt;h3&gt;
  
  
  A Real Example — dev.to Is Already Doing This
&lt;/h3&gt;

&lt;p&gt;Every image you upload gets stored on S3 at full resolution.&lt;/p&gt;

&lt;p&gt;That's not what your readers download.&lt;/p&gt;

&lt;p&gt;Cloudflare intercepts every request, resizes the image, converts the format, and sends the smaller version. The original never changes. The user never sees it.&lt;/p&gt;

&lt;p&gt;The URL tells the whole story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You uploaded:
https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/your-image.gif

Your readers get:
https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ydkr1d4qmhxyms63lpx.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;width=1000,height=420&lt;/code&gt;. &lt;code&gt;format=auto&lt;/code&gt;. &lt;code&gt;fit=cover&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Resize. Convert. Crop. All at the edge, before a single byte reaches your reader.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5xsfhh9cd4zlln49ofu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5xsfhh9cd4zlln49ofu.png" alt="Side-by-side DevTools comparison: CDN version (left) at 147kB loads in 212ms, S3 original (right) at 22,759kB takes 10.13s — same image, 155× smaller and 48× faster through Cloudflare" width="799" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same image. Two URLs. Here's what that costs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CDN (what readers get)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;147 kB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;212ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 original (what you uploaded)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22,759 kB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.13s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's &lt;strong&gt;155× smaller&lt;/strong&gt; and &lt;strong&gt;48× faster&lt;/strong&gt;.&lt;br&gt;
Not from any code change. From a URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dev.to is doing automatically what this section teaches you to do manually.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They do it for images.&lt;br&gt;
They do it for embeds too.&lt;/p&gt;

&lt;p&gt;Every CodePen, every YouTube embed — none of them load until you scroll to them.&lt;/p&gt;

&lt;p&gt;Open DevTools. Watch the Network tab. Scroll slowly down the page.&lt;/p&gt;

&lt;p&gt;You'll see requests fire at the exact moment each embed enters the viewport. Not a second before.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajtjj9kim1tpsffyj1jc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajtjj9kim1tpsffyj1jc.png" alt="DevTools Timing panel showing a CodePen embed queued at 18.7 minutes — it only loaded the moment I scrolled to it, not at page load. Once requested, Cloudflare served it from edge cache in 47µs." width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That "Queued at 18.7 min" is the exact moment I scrolled to the embed.&lt;/p&gt;

&lt;p&gt;Not at page load. Not before. The second it entered the viewport — the request fired.&lt;/p&gt;

&lt;p&gt;And it resolved in &lt;strong&gt;47 microseconds&lt;/strong&gt;. Cloudflare already had it cached at the edge.&lt;/p&gt;

&lt;p&gt;That's intersection observer lazy loading for iframes. Same principle as &lt;code&gt;loading="lazy"&lt;/code&gt; on images. dev.to just applies it to everything.&lt;/p&gt;


&lt;h2&gt;
  
  
  Fonts Cost More Than You Think &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Images delay pixels.&lt;br&gt;
JavaScript delays interaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom fonts delay reading.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The browser reaches your text.&lt;br&gt;
Your font isn't there yet.&lt;/p&gt;

&lt;p&gt;On a cold connection, Google Fonts alone can add &lt;strong&gt;300ms to your LCP&lt;/strong&gt; before a single character appears.&lt;/p&gt;

&lt;p&gt;Now it has a few choices.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FOIT&lt;/strong&gt; (Flash of Invisible Text) — hide the text until the font finishes downloading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FOUT&lt;/strong&gt; (Flash of Unstyled Text) — show a fallback font, then swap when the custom font arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;font-display: optional&lt;/code&gt;&lt;/strong&gt; — give the custom font a brief window to show up; if it isn't ready in time, stick with the fallback for the rest of the visit and skip the swap entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them are perfect.&lt;br&gt;
The goal is to make the compromise invisible.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;font-display: swap&lt;/code&gt; chooses readability over visual perfection.&lt;/p&gt;

&lt;p&gt;The user starts reading immediately.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/utkarshbansal01/embed/OPWjgBa?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Press &lt;strong&gt;Play&lt;/strong&gt; and watch all three strategies side by side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Inter'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/fonts/inter.woff2')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'woff2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;swap&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;Use &lt;code&gt;woff2&lt;/code&gt;. It's smaller than older formats and supported by every modern browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One catch:&lt;/strong&gt; If your fallback font has different metrics, swapping fonts causes layout shift. That's textbook CLS.&lt;/p&gt;

&lt;p&gt;Properties like &lt;code&gt;size-adjust&lt;/code&gt;, &lt;code&gt;ascent-override&lt;/code&gt;, and &lt;code&gt;descent-override&lt;/code&gt; in the &lt;code&gt;@font-face&lt;/code&gt; rule let you tune the fallback so the swap barely moves.&lt;/p&gt;

&lt;p&gt;You can also give the browser a head start.&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="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fonts/inter.woff2"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"font/woff2"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The font starts downloading before CSS asks for it.&lt;br&gt;
Or...&lt;br&gt;
hear me out — &lt;strong&gt;use no custom font&lt;/strong&gt; at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;-apple-system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BlinkMacSystemFont&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;'Segoe UI'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Roboto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&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;System fonts are already available on the user's device.&lt;/p&gt;

&lt;p&gt;Zero downloads. Zero waiting.&lt;/p&gt;

&lt;p&gt;GitHub, Notion, dev.to — all use system fonts. So why can't you?&lt;/p&gt;

&lt;p&gt;The fastest font is the one you never download.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Three chapters, one rule.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The fastest JavaScript never downloads.&lt;br&gt;
The fastest image is the smallest one.&lt;br&gt;
The fastest font is already installed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next bottleneck follows the same rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Except now...&lt;/strong&gt; The delay isn't on your page.&lt;/p&gt;

&lt;p&gt;It's on the wire.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Network Is Not Your Friend — Until You Work With It &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;You've trimmed the JavaScript. Optimized the CSS. Shrunk the images. Fixed the fonts.&lt;/p&gt;

&lt;p&gt;The bottleneck moves — &lt;strong&gt;to the network.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  TTFB — The Delay Before Anything Happens
&lt;/h3&gt;

&lt;p&gt;Before the browser can download CSS, discover JavaScript or request your hero image...&lt;/p&gt;

&lt;p&gt;It waits for the server to start responding.&lt;br&gt;
That's &lt;strong&gt;Time to First Byte (TTFB).&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Good       -&amp;gt; under 800ms
Needs work -&amp;gt; 800ms – 1.8s
Poor       -&amp;gt; over 1.8s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your TTFB is two seconds...&lt;br&gt;
Everything else starts two seconds late.&lt;br&gt;
Frontend optimizations haven't even begun.&lt;/p&gt;

&lt;p&gt;It might sound like a backend concern — until you see what it cost Amazon.&lt;/p&gt;

&lt;p&gt;Every 100ms of added latency cost Amazon 1% in sales.&lt;br&gt;
Not 1% of a bad quarter.&lt;/p&gt;

&lt;p&gt;1% of revenue, every single time, from a tenth of a second.&lt;/p&gt;

&lt;p&gt;At Amazon's scale today, that 1% is worth billions of dollars a year.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Clock it:&lt;/strong&gt; DevTools -&amp;gt; Network -&amp;gt; Reload -&amp;gt; Click the HTML document -&amp;gt; &lt;strong&gt;Timing&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faj62yutnae9rxdkcnk35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faj62yutnae9rxdkcnk35.png" alt="Network Timing panel — TTFB" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Waiting for server response" bar is your TTFB.&lt;br&gt;
Over &lt;strong&gt;800ms&lt;/strong&gt; means your server is now the bottleneck.&lt;br&gt;
No amount of frontend optimization can hide it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four common causes — all fixable:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow database queries&lt;/li&gt;
&lt;li&gt;No caching&lt;/li&gt;
&lt;li&gt;No CDN&lt;/li&gt;
&lt;li&gt;Servers too far from the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;strong&gt;CDN&lt;/strong&gt; (Content Delivery Network) solves the distance problem.&lt;/p&gt;

&lt;p&gt;Instead of serving every request from one origin server, it serves files from an edge location close to the user. That's also why you'll often see region-specific domains like amazon.in and amazon.com alongside CDNs and regional data centers — shorter distances mean lower latency.&lt;/p&gt;

&lt;p&gt;For global audiences, that's often a &lt;strong&gt;200–400ms&lt;/strong&gt; win before you've touched a line of code.&lt;/p&gt;

&lt;p&gt;Platforms like Vercel, Netlify and Cloudflare already do this automatically.&lt;/p&gt;

&lt;p&gt;If your server lives in one region while your users live everywhere...&lt;/p&gt;

&lt;p&gt;A CDN is usually the highest-ROI optimization you can make.&lt;/p&gt;
&lt;h3&gt;
  
  
  Gzip, Brotli and Zstd — Free Bandwidth
&lt;/h3&gt;

&lt;p&gt;Before files leave your server...&lt;br&gt;
Compress them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original JS:   500 KB
After minify:  200 KB  (bundler handles this)
After gzip:     60 KB  (server handles this — 70% off)
After Brotli:   50 KB  (15–25% better than gzip)
After zstd:     48 KB  (newer than Brotli — what dev.to uses)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minification removes unnecessary characters.&lt;br&gt;
Compression removes unnecessary bytes.&lt;/p&gt;

&lt;p&gt;Different job. Much bigger payoff.&lt;/p&gt;

&lt;p&gt;Most CDNs enable compression automatically. Check yours:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Peek at the headers:&lt;/strong&gt; DevTools -&amp;gt; Network -&amp;gt; Reload -&amp;gt; click any &lt;code&gt;.js&lt;/code&gt; file -&amp;gt; &lt;strong&gt;Response Headers&lt;/strong&gt; -&amp;gt; look for &lt;code&gt;Content-Encoding&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdxvuujuprri9yqan8izx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdxvuujuprri9yqan8izx.png" alt="Response headers showing content-encoding" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;br&lt;/code&gt; -&amp;gt; Brotli&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gzip&lt;/code&gt; -&amp;gt; Gzip&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zstd&lt;/code&gt; -&amp;gt; Zstandard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any of them means compression is active.&lt;/p&gt;

&lt;p&gt;dev.to already serves assets with &lt;code&gt;zstd&lt;/code&gt;. If a platform serving millions of developers made the switch, the tooling is mature enough for your project too.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;Content-Encoding&lt;/code&gt; header?&lt;/p&gt;

&lt;p&gt;Means your server is sending raw files — you're paying roughly a &lt;strong&gt;70% bandwidth penalty&lt;/strong&gt; for every request.&lt;/p&gt;

&lt;p&gt;If you're using Vercel, Netlify, or Cloudflare... you're probably already covered.&lt;/p&gt;

&lt;p&gt;No code change. Just a config line.&lt;br&gt;
Most sites don't bother. That's why it's still a win.&lt;/p&gt;

&lt;p&gt;On your own server, it's almost trivial — for gzip, at least.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Nginx&lt;/span&gt;
&lt;span class="k"&gt;gzip&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;brotli&lt;/span&gt; &lt;span class="no"&gt;on&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 apache"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apache&lt;/span&gt;
&lt;span class="nc"&gt;AddOutputFilterByType&lt;/span&gt; DEFLATE text/html text/css application/javascript image/svg+xml application/json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gzip ships with nginx out of the box. Brotli doesn't — &lt;code&gt;brotli on;&lt;/code&gt; will fail to start nginx unless you've already compiled in the separate &lt;code&gt;ngx_brotli&lt;/code&gt; module (or you're on NGINX Plus, or using a distro package like &lt;code&gt;nginx-extras&lt;/code&gt; on Debian/Ubuntu that bundles it in for you).&lt;/p&gt;

&lt;p&gt;As for &lt;strong&gt;Zstandard (&lt;code&gt;zstd&lt;/code&gt;)&lt;/strong&gt;, support depends on your server or CDN. Many modern CDNs already enable it automatically, while self-hosted servers often require additional modules or newer server software.&lt;/p&gt;




&lt;h3&gt;
  
  
  Preload, Prefetch, Preconnect — Know the Difference
&lt;/h3&gt;

&lt;p&gt;These three are easy to confuse.&lt;br&gt;
They solve completely different problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Preload&lt;/strong&gt; -&amp;gt; &lt;em&gt;I need this on this page.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefetch&lt;/strong&gt; -&amp;gt; &lt;em&gt;I'll probably need this on the next page.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preconnect&lt;/strong&gt; -&amp;gt; &lt;em&gt;I'm about to talk to this server, just open the connection so it's ready when I knock.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&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;!-- Preload: I need this RIGHT NOW on the current page --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/hero.webp"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fonts/font.woff2"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Prefetch: I'll probably need this when the user navigates NEXT --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"prefetch"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/about.js"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Preconnect: warm up the connection to this domain --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preconnect"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preconnect"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.myapp.com"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- DNS lookup only: cheaper preconnect — resolves DNS only --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"dns-prefetch"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://analytics.myapp.com"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Use the wrong one and you waste bandwidth. Skip them entirely and you waste time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hint&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hero image, critical font, above-the-fold CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prefetch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Likely next page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;preconnect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Third-party origins you'll definitely contact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dns-prefetch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Third-party origins you might contact&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One &lt;code&gt;&amp;lt;link rel="preconnect"&amp;gt;&lt;/code&gt; eliminates the DNS, TCP, and TLS cost upfront. That's typically &lt;strong&gt;100–300ms saved&lt;/strong&gt; per domain.&lt;/p&gt;

&lt;p&gt;One warning: &lt;code&gt;preload&lt;/code&gt; is a priority override — every preloaded resource jumps to the front of the queue.&lt;/p&gt;

&lt;p&gt;Preload ten things and you've prioritized nothing.&lt;/p&gt;


&lt;h3&gt;
  
  
  Cache Everything That Doesn't Change
&lt;/h3&gt;

&lt;p&gt;The browser already has an excellent cache.&lt;br&gt;
Most sites barely use it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: no-cache                    -&amp;gt; always validate with server
Cache-Control: max-age=31536000            -&amp;gt; cache for one year
Cache-Control: max-age=31536000, immutable -&amp;gt; cache forever, never revalidate
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern never changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;HTML files     -&amp;gt; Cache-Control: no-cache
                 (always check for new HTML — it references
                 updated asset URLs)

JS/CSS/Images  -&amp;gt; Cache-Control: max-age=31536000, immutable
                 (cached forever — content hash in filename
                 changes on deploy)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern bundlers already fingerprint assets. (app.a3f5b2c.js)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.js
↓
app.a3f5b2c.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;New deploy, new hash.&lt;br&gt;
Same hash, instant cache hit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The browser downloads it once.&lt;br&gt;
Everything else stays cached forever.&lt;br&gt;
Here's what that looks like on a repeat visit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Watch it stick:&lt;/strong&gt; DevTools -&amp;gt; Network -&amp;gt; reload twice -&amp;gt; watch the &lt;strong&gt;Size&lt;/strong&gt; column.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk2x16g0rnf2oz7oq6vh0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk2x16g0rnf2oz7oq6vh0.png" alt="Memory cache at 0ms vs network requests at 371ms–1s" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cached resources return instantly: &lt;strong&gt;0ms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Network requests still take: &lt;strong&gt;371ms to over 1 second&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Same files.&lt;br&gt;
Zero download.&lt;br&gt;
That's exactly what &lt;code&gt;immutable&lt;/code&gt; buys you.&lt;br&gt;
Configuration depends on your host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# Netlify — netlify.toml&lt;/span&gt;
&lt;span class="nn"&gt;[[headers]]&lt;/span&gt;
  &lt;span class="py"&gt;for&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/assets/*"&lt;/span&gt;
  &lt;span class="nn"&gt;[headers.values]&lt;/span&gt;
    &lt;span class="py"&gt;Cache-Control&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"max-age&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;31536000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;immutable&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="nn"&gt;[[headers]]&lt;/span&gt;
  &lt;span class="py"&gt;for&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/*.html"&lt;/span&gt;
  &lt;span class="nn"&gt;[headers.values]&lt;/span&gt;
    &lt;span class="py"&gt;Cache-Control&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"no-cache"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;Vercel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vercel.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;"headers"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/assets/(.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&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;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cache-Control"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"max-age=31536000, immutable"&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;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;One deploy. One download. Every visit after that is free.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Back/Forward Cache — The Fastest Navigation You'll Ever Ship
&lt;/h3&gt;

&lt;p&gt;What's faster than caching files?&lt;br&gt;
Not loading the page at all.&lt;/p&gt;

&lt;p&gt;That's exactly what the &lt;strong&gt;Back/Forward Cache (bfcache)&lt;/strong&gt; does.&lt;/p&gt;

&lt;p&gt;Press Back.&lt;/p&gt;

&lt;p&gt;The browser restores the entire page from memory.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No network.&lt;/li&gt;
&lt;li&gt;No rendering.&lt;/li&gt;
&lt;li&gt;No JavaScript startup.&lt;/li&gt;
&lt;li&gt;Usually under &lt;strong&gt;100ms&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most developers break it without realizing.&lt;br&gt;
&lt;strong&gt;The biggest culprit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;unload&lt;/code&gt;.&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;// 🔴 Disables bfcache&lt;/span&gt;
&lt;span class="nb"&gt;window&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// 🟢 Compatible with bfcache&lt;/span&gt;
&lt;span class="nb"&gt;window&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pagehide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many analytics libraries still register &lt;code&gt;unload&lt;/code&gt; handlers behind the scenes.&lt;/p&gt;

&lt;p&gt;One script.&lt;br&gt;
Your entire site loses bfcache.&lt;br&gt;
Check yours.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 DevTools -&amp;gt; Application -&amp;gt; Back/Forward Cache -&amp;gt; Run Test&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Passing: dev.to&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpw5sbn319rb012gasgc0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpw5sbn319rb012gasgc0.png" alt="dev.to: bfcache pass" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Failing: CodePen&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6als6lmqa7ls2dhr4t8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl6als6lmqa7ls2dhr4t8.png" alt="CodePen: bfcache fail" width="800" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The report tells you exactly what prevented caching.&lt;br&gt;
Fix what it reports.&lt;/p&gt;

&lt;p&gt;One exception: banking, payments, medical portals — anything showing sensitive data.&lt;br&gt;
In those cases, restoring an old page from memory may be the wrong trade-off.&lt;/p&gt;

&lt;p&gt;Security wins.&lt;/p&gt;

&lt;p&gt;For everything else... bfcache should work by default.&lt;/p&gt;


&lt;h3&gt;
  
  
  One Move, Five Times
&lt;/h3&gt;

&lt;p&gt;Every optimization in this chapter does exactly the same thing.&lt;/p&gt;

&lt;p&gt;It avoids work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CDN avoids the long trip to your origin server.&lt;/li&gt;
&lt;li&gt;Compression avoids sending unnecessary bytes.&lt;/li&gt;
&lt;li&gt;Preconnect avoids waiting for a connection to open.&lt;/li&gt;
&lt;li&gt;Caching avoids downloading files twice.&lt;/li&gt;
&lt;li&gt;bfcache avoids loading the page altogether.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The fastest request&lt;/strong&gt; isn't the one you optimize.&lt;/p&gt;

&lt;p&gt;It's the one the &lt;strong&gt;browser never has to make&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Framework Patterns That Actually Move the Needle &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Everything so far was browser fundamentals.&lt;/p&gt;

&lt;p&gt;Now let's look at what your framework is already doing for you.&lt;/p&gt;

&lt;p&gt;Here's what many developers miss.&lt;/p&gt;

&lt;p&gt;React and Vue aren't fast because they're frameworks.&lt;br&gt;
They're fast because they avoid unnecessary work.&lt;br&gt;
Out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React&lt;/strong&gt; batches state updates (React 18+) so multiple &lt;code&gt;setState()&lt;/code&gt; calls become a single render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vue&lt;/strong&gt; batches reactive updates and flushes DOM changes together on the next tick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both&lt;/strong&gt; diff a virtual DOM before touching the real DOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Less work.&lt;br&gt;
Fewer DOM updates.&lt;br&gt;
Better performance.&lt;br&gt;
Most applications never need more than that.&lt;/p&gt;

&lt;p&gt;The techniques below are for when the defaults stop being enough.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR — what this chapter covers:&lt;/strong&gt;&lt;br&gt;
React and Vue already optimize rendering by default. This chapter covers three things for when that's not enough: memoization, conditional rendering (v-if vs v-show), and virtualization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Angular, Svelte, or Solid?&lt;/strong&gt; Same ideas, different APIs — I don't have enough production experience to cover them accurately.&lt;br&gt;
Skip to Perceived Performance&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h3&gt;
  
  
  Stop Re-rendering What Didn't Change
&lt;/h3&gt;

&lt;p&gt;The fastest render... is the one that &lt;strong&gt;never happens&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React&lt;/strong&gt; skips re-rendering with &lt;code&gt;React.memo()&lt;/code&gt; — it wraps a component and bails out if props haven't changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vue&lt;/strong&gt; handles this differently. Its reactivity system is more granular by default — components only re-render when their reactive dependencies change. For expensive computed values, &lt;code&gt;computed()&lt;/code&gt; caches the result and only recalculates when its dependencies change.&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;// 🟢 React — skip re-rendering when props are the same&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DataGrid&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;DataGrid&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rows&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;rows&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;row&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;Row&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&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="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;)
&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 🔴 React — comparison overhead outweighs the benefit&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Badge&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;Badge&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;label&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&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;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&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;// 🟢 Vue — cached until dependencies change&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&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;cart&lt;/span&gt;&lt;span class="p"&gt;.&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;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&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;price&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="c1"&gt;// 🔴 Vue — recalculates every render&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&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;gt;&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&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;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&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;price&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part most tutorials skip: memoization is not free.&lt;/p&gt;

&lt;p&gt;Every memoized component first compares its inputs before deciding to skip rendering.&lt;/p&gt;

&lt;p&gt;If the component is cheap, that comparison can cost more than rendering itself.&lt;/p&gt;

&lt;p&gt;Good candidates are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large lists&lt;/li&gt;
&lt;li&gt;Heavy components&lt;/li&gt;
&lt;li&gt;Expensive calculations&lt;/li&gt;
&lt;li&gt;Deep trees with stable props&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else?&lt;br&gt;
Measure first.&lt;/p&gt;


&lt;h3&gt;
  
  
  v-if vs. v-show — Don't Default to One
&lt;/h3&gt;

&lt;p&gt;Vue has an optimization most developers get wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- 🔴 Destroy and recreate every toggle --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ExpensiveModal&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isOpen"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- 🟢 Keep mounted, toggle visibility --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ExpensiveModal&lt;/span&gt; &lt;span class="na"&gt;v-show=&lt;/span&gt;&lt;span class="s"&gt;"isOpen"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rarely shown?&lt;br&gt;
-&amp;gt; Use &lt;code&gt;v-if&lt;/code&gt;. The component never mounts.&lt;/p&gt;

&lt;p&gt;Frequently toggled?&lt;br&gt;
-&amp;gt; Use &lt;code&gt;v-show&lt;/code&gt;. It stays in the DOM, just hidden.&lt;/p&gt;

&lt;p&gt;Don't guess.&lt;/p&gt;

&lt;p&gt;React DevTools Profiler and Vue Devtools will tell you exactly where your renders are coming from.&lt;/p&gt;


&lt;h3&gt;
  
  
  Virtualization — The DOM Was Never Meant for 10,000 Rows
&lt;/h3&gt;

&lt;p&gt;Imagine a city-select dropdown with 10,000 options.&lt;br&gt;
The user can only see 15 at a time.&lt;/p&gt;

&lt;p&gt;The other 9,985:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Still consuming memory.&lt;/li&gt;
&lt;li&gt;Still participating in layout.&lt;/li&gt;
&lt;li&gt;Still slowing everything down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't render them.&lt;/strong&gt;&lt;br&gt;
Render only what's visible.&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;// React&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;FixedSizeList&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;List&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;react-window&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;itemCount&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;itemSize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;50&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="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;style&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&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="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;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 vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;RecycleScroller&lt;/span&gt; &lt;span class="na"&gt;:items=&lt;/span&gt;&lt;span class="s"&gt;"rows"&lt;/span&gt; &lt;span class="na"&gt;:item-size=&lt;/span&gt;&lt;span class="s"&gt;"50"&lt;/span&gt; &lt;span class="na"&gt;v-slot=&lt;/span&gt;&lt;span class="s"&gt;"{ item }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Row&lt;/span&gt; &lt;span class="na"&gt;:data=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/RecycleScroller&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten thousand rows in memory.&lt;br&gt;
Fifteen rows in the DOM.&lt;br&gt;
&lt;strong&gt;That's virtualization.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Perceived Performance — Making It Feel Fast Before It Is &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Technical performance improvements make pages faster.&lt;br&gt;
Perceived performance makes them &lt;strong&gt;feel even faster&lt;/strong&gt;.&lt;br&gt;
Those aren't always the same thing.&lt;/p&gt;

&lt;p&gt;A 1.5-second API request behind a spinner feels slow.&lt;br&gt;
The same 1.5 seconds with an optimistic update feels instant.&lt;/p&gt;

&lt;p&gt;The network didn't change.&lt;br&gt;
The experience did.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/utkarshbansal01/embed/ZYLJymQ?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Race them:&lt;/strong&gt; Which one feels fastest?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's perceived performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimistic Updates — Respond Before the Server Does
&lt;/h3&gt;

&lt;p&gt;Most applications wait for server confirmation before updating the UI.&lt;/p&gt;

&lt;p&gt;Optimistic updates reverse that order.&lt;br&gt;
The UI updates immediately.&lt;br&gt;
The server catches up afterward.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;likePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setLiked&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="nf"&gt;setLikeCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;count&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;likePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&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;setLiked&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="nf"&gt;setLikeCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;count&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="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;If the request fails... &lt;strong&gt;Roll it back.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub stars.&lt;br&gt;
Notion edits.&lt;br&gt;
They all update before the server responds.&lt;/p&gt;

&lt;p&gt;In Instagram's earlier days, a single Justin Bieber post could pull in millions of simultaneous like-taps within seconds — more than enough to choke a database trying to keep an accurate count in real time.&lt;/p&gt;

&lt;p&gt;That's a backend problem. It needed its own backend fix.&lt;br&gt;
But it also exposed a frontend one: every tap was a person waiting on a network round-trip just to see a number move.&lt;/p&gt;

&lt;p&gt;Optimistic updates fixed that part.&lt;/p&gt;

&lt;p&gt;The number you see immediately after pressing ❤️ is often an optimistic estimate that eventually catches up.&lt;/p&gt;

&lt;p&gt;It feels instant because your tap isn't waiting for the database anymore.&lt;/p&gt;


&lt;h3&gt;
  
  
  Stale While Revalidate — Show Something Now
&lt;/h3&gt;

&lt;p&gt;Returning users shouldn't stare at another loading spinner.&lt;/p&gt;

&lt;p&gt;They've already seen this data.&lt;br&gt;
Show the cached version immediately.&lt;br&gt;
Refresh it quietly in the background.&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;// TanStack Query / SWR&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;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&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="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fetchDashboardData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&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;First visit -&amp;gt; normal load.&lt;br&gt;
Every next visit -&amp;gt; instant content from cache.&lt;br&gt;
Silent refresh.&lt;/p&gt;

&lt;p&gt;Libraries like &lt;strong&gt;TanStack Query&lt;/strong&gt; and &lt;strong&gt;SWR&lt;/strong&gt; make this almost effortless.&lt;/p&gt;

&lt;p&gt;You see it in the Instagram feed too — the content from your last visit appears instantly while fresh posts load in the background.&lt;/p&gt;


&lt;h3&gt;
  
  
  Prefetch Before the Click
&lt;/h3&gt;

&lt;p&gt;Remember the &lt;code&gt;prefetch&lt;/code&gt; hint from a few chapters back? That was a guess about the next page.&lt;/p&gt;

&lt;p&gt;People rarely click instantly.&lt;br&gt;
Most hover for &lt;strong&gt;200–300ms&lt;/strong&gt; first.&lt;br&gt;
&lt;strong&gt;That's free time. Use it.&lt;/strong&gt;&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[data-prefetch]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&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;link&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mouseenter&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="o"&gt;=&amp;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;preload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prefetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="nx"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preload&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="na"&gt;once&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;By the time the click happens...&lt;br&gt;
Part of the next page is already downloading.&lt;/p&gt;

&lt;p&gt;Next.js does this automatically for internal links.&lt;/p&gt;


&lt;h3&gt;
  
  
  Skeletons Beat Spinners
&lt;/h3&gt;

&lt;p&gt;A spinner tells the user...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Something is happening."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A skeleton tells them...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This is what you're waiting for. The content is almost here."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The loading time is identical.&lt;/p&gt;

&lt;p&gt;Research by Viget found users rate skeleton screens as &lt;strong&gt;20% faster&lt;/strong&gt; than spinners — even when the actual load time is identical.&lt;/p&gt;

&lt;p&gt;Not because they are.&lt;br&gt;
Because uncertainty feels slower than progress.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;A different kind of win this time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;None of these techniques make the server faster.&lt;br&gt;
They change &lt;strong&gt;when&lt;/strong&gt; the user experiences the wait.&lt;br&gt;
Sometimes that's enough.&lt;/p&gt;

&lt;p&gt;Because performance isn't only measured in milliseconds.&lt;/p&gt;

&lt;p&gt;It's measured in how long the wait &lt;strong&gt;feels&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Lighthouse Is Where You Start. Field Data Is Where You Trust. &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;You've spent the whole article learning &lt;strong&gt;what&lt;/strong&gt; to optimize.&lt;/p&gt;

&lt;p&gt;Now let's learn &lt;strong&gt;how to find it.&lt;/strong&gt;&lt;br&gt;
Start with Lighthouse.&lt;br&gt;
Run it before every release.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Run it now:&lt;/strong&gt; DevTools -&amp;gt; Lighthouse -&amp;gt; Mobile -&amp;gt; Analyze page load.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpoig4o0ekr2liltasq7e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpoig4o0ekr2liltasq7e.png" alt="Lighthouse score 81: FCP 3.2s, LCP 4.0s, TBT 10ms, CLS 0" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ignore the score.&lt;br&gt;
The real value is underneath.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FCP.&lt;/li&gt;
&lt;li&gt;LCP.&lt;/li&gt;
&lt;li&gt;TBT.&lt;/li&gt;
&lt;li&gt;CLS.&lt;/li&gt;
&lt;li&gt;Speed Index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one points to a specific bottleneck.&lt;br&gt;
Scroll a little further.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmb5e2fdw4ka7v108orl1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmb5e2fdw4ka7v108orl1.png" alt="Lighthouse diagnostics: render-blocking, unused JS, long tasks, bfcache blocked" width="799" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lighthouse doesn't just tell you something is slow.&lt;br&gt;
It tells you why.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Render-blocking resources.&lt;/li&gt;
&lt;li&gt;Unused JavaScript.&lt;/li&gt;
&lt;li&gt;Long Tasks.&lt;/li&gt;
&lt;li&gt;Broken bfcache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prioritized. Estimated. Actionable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One caveat:&lt;/strong&gt; that 81 score came from one controlled test.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One device.&lt;/li&gt;
&lt;li&gt;One network.&lt;/li&gt;
&lt;li&gt;One page load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your users don't browse under laboratory conditions.&lt;/p&gt;

&lt;p&gt;They browse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On trains.&lt;/li&gt;
&lt;li&gt;In elevators.&lt;/li&gt;
&lt;li&gt;On overloaded mobile networks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why field data exists.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔧 Check it yourself:&lt;/strong&gt; Go to PageSpeed Insights &lt;a href="https://pagespeed.web.dev" rel="noopener noreferrer"&gt;pagespeed.web.dev&lt;/a&gt;, paste any URL, then look for &lt;strong&gt;"Discover what your real users are experiencing."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob8w2uwc4up18a89onjj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob8w2uwc4up18a89onjj.png" alt="PageSpeed Insights: field data vs lab score" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice something.&lt;/p&gt;

&lt;p&gt;The top section isn't Lighthouse.&lt;br&gt;
It's &lt;strong&gt;CrUX&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real Chrome users.&lt;/li&gt;
&lt;li&gt;Real devices.&lt;/li&gt;
&lt;li&gt;The last 28 days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The score underneath is still Lighthouse.&lt;br&gt;
Same page.&lt;br&gt;
Different measurement.&lt;/p&gt;

&lt;p&gt;In this example,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This URL&lt;/strong&gt; measures only the homepage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin&lt;/strong&gt; averages every page on dev.to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same site.&lt;br&gt;
Different data.&lt;br&gt;
Different conclusions.&lt;/p&gt;

&lt;p&gt;Lighthouse tells you what's possible.&lt;br&gt;
Field data tells you what's happening.&lt;/p&gt;

&lt;p&gt;When they disagree... &lt;strong&gt;Trust the field.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you've found the problem, you've already learned how to investigate it.&lt;/p&gt;

&lt;p&gt;Open the Performance panel. -&amp;gt; Record. -&amp;gt; Find the bottleneck. -&amp;gt; Fix it.&lt;/p&gt;

&lt;p&gt;Finally, measure your own users:&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="nx"&gt;onINP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onCLS&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="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="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&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&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="nf"&gt;onINP&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="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INP&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="nf"&gt;onCLS&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="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CLS&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send those metrics to whatever analytics platform you already use.&lt;/p&gt;

&lt;p&gt;Then watch the &lt;strong&gt;p75&lt;/strong&gt; — not the average, but the point where 75% of visits were this fast or faster.&lt;br&gt;
That's the number Google uses for Core Web Vitals scoring.&lt;br&gt;
And it's the one your users actually experience.&lt;/p&gt;

&lt;p&gt;One more tool: &lt;strong&gt;WebPageTest&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Lighthouse covers most days. WebPageTest goes deeper — filmstrip, waterfall, real device in a specific city on a throttled connection.&lt;/p&gt;

&lt;p&gt;Same site. Same browser. Same connection speed. Tested from two cities on opposite sides of the planet:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8g4d50p5ae5773plt1vj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8g4d50p5ae5773plt1vj.png" alt="Page Performance summary comparing dev.to tested from Tokyo, Japan vs Columbus, Ohio, USA — First Contentful Paint, Largest Contentful Paint, Time to First Byte, and other core metrics side by side" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tokyo and Columbus, Ohio — and the numbers barely move. That's a CDN doing its job.&lt;/p&gt;

&lt;p&gt;Full reports: &lt;a href="https://www.catchpoint.com/webpagetest/results?publicurl=https%3A%2F%2Fpublic.catchpoint.com%2FUI%2FEntry%2FWPTITP%2FARRe-D-E-B2AC5WCje18zcJoAA-N" rel="noopener noreferrer"&gt;Tokyo run&lt;/a&gt; · &lt;a href="https://www.catchpoint.com/webpagetest/results?publicurl=https%3A%2F%2Fpublic.catchpoint.com%2FUI%2FEntry%2FWPTITP%2FARRa-D-E-B2AC5V3je18zcJoAA-N" rel="noopener noreferrer"&gt;Columbus run&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Performance Budget — Make It Impossible to Regress &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the problem — you fix performance, the numbers look great, then it quietly rots.&lt;/p&gt;

&lt;p&gt;Performance doesn't usually break overnight.&lt;br&gt;
It leaks.&lt;/p&gt;

&lt;p&gt;One dependency.&lt;br&gt;
One analytics script.&lt;br&gt;
One feature.&lt;/p&gt;

&lt;p&gt;Sprint after sprint.&lt;/p&gt;

&lt;p&gt;Your Lighthouse score is &lt;strong&gt;92&lt;/strong&gt; today.&lt;br&gt;
Six months later it's &lt;strong&gt;58&lt;/strong&gt;.&lt;br&gt;
Nobody remembers when it happened.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;performance budget&lt;/strong&gt; fixes that.&lt;/p&gt;

&lt;p&gt;It's a contract enforced by CI.&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;// lighthouserc.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;assertions&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;categories:performance&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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;minScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;largest-contentful-paint&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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxNumericValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2500&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;total-blocking-time&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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxNumericValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cumulative-layout-shift&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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxNumericValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resource-summary:script:size&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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxNumericValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300000&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="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;Five limits. Miss any one, and CI flags it before a reviewer has to.&lt;/p&gt;

&lt;p&gt;Wire it into your pipeline — and performance stops being someone's responsibility.&lt;/p&gt;

&lt;p&gt;Cross the limit.&lt;br&gt;
The pipeline fails.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Uncomfortable Part Nobody Puts in the Tutorial
&lt;/h2&gt;

&lt;p&gt;Every optimization you've seen has trade-offs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lazy-loading your hero image&lt;/strong&gt; delays your LCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrapping every component in &lt;code&gt;memo&lt;/code&gt;&lt;/strong&gt; adds comparison overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preloading everything&lt;/strong&gt; means nothing is truly prioritized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR&lt;/strong&gt; improves LCP but introduces hydration work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-splitting bundles&lt;/strong&gt; creates unnecessary network overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance advice isn't universal. It's contextual.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before changing anything, ask one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Am I solving a problem I've actually measured, for users who actually have it?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you can't answer that — that's not optimization.&lt;/p&gt;

&lt;p&gt;That's a guessing game.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The most expensive optimization is the one that improves a metric nobody was watching.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  The Checklist You'll Actually Use &lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;One article. Twelve chapters. Hundreds of decisions.&lt;br&gt;
Here's all of it in one place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server
  ✓ Keep TTFB under 800 ms                           (imp)
  ✓ Serve through a CDN                              (imp)
  ✓ Enable Brotli or Gzip or Zstd                    (imp)
Browser Rendering
  ✓ Remove render-blocking CSS and JS                (imp)
  ✓ Use defer for app scripts                        (imp)
  ✓ Inline critical CSS
JavaScript
  ✓ Code split by route                              (imp)
  ✓ Tree shake dependencies                          (imp)
  ✓ Analyze bundles before release
  ✓ Use passive listeners
  ✓ Load third-party scripts with async              (imp)
  ✓ Move heavy work to Web Workers
  ✓ Break up long tasks                              (imp)
CSS
  ✓ Animate transform and opacity only               (imp)
  ✓ Avoid layout thrashing
  ✓ Use CSS containment
  ✓ Use content-visibility for off-screen content
Images
  ✓ Prefer AVIF or WebP                              (imp)
  ✓ Always set width and height                      (imp)
  ✓ fetchpriority="high" for the LCP image           (imp)
  ✓ Lazy-load below the fold                         (imp)
  ✓ Use srcset
Fonts
  ✓ Preload critical fonts                           (imp)
  ✓ Use font-display: swap                           (imp)
  ✓ Prefer system fonts when possible
Network
  ✓ Preconnect to third-party origins
  ✓ Prefetch likely next pages
  ✓ Cache hashed assets aggressively                 (imp)
  ✓ Keep bfcache working                             (imp)
Framework
  ✓ Memoize expensive components only                (imp)
  ✓ Virtualize long lists
Perceived Performance
  ✓ Optimistic updates
  ✓ Stale-while-revalidate
  ✓ Prefetch on hover
  ✓ Skeleton screens                                 (imp)
Measurement
  ✓ Lighthouse CI                                    (imp)
  ✓ web-vitals in production                         (imp)
  ✓ Track p75 field data                             (imp)
  ✓ Verify with WebPageTest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every item applies to every project.&lt;/p&gt;

&lt;p&gt;But every item you skip should be a choice you made — not an oversight.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Performance isn't about making browsers faster.&lt;/p&gt;

&lt;p&gt;It's about giving them less work to do.&lt;/p&gt;

&lt;p&gt;Less JavaScript to parse.&lt;br&gt;
Less CSS to calculate.&lt;br&gt;
Fewer images to download.&lt;br&gt;
Fewer layouts to recalculate.&lt;br&gt;
Fewer requests to make.&lt;/p&gt;

&lt;p&gt;Every optimization in this article follows the same idea.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because every piece of work you remove... is time your user gets back.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Three seconds.&lt;/strong&gt;&lt;br&gt;
You don't earn them back with one optimization.&lt;br&gt;
You earn them back a hundred milliseconds at a time.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
      <category>frontend</category>
    </item>
    <item>
      <title>**What's the most over-engineered AI solution you've seen for a problem a linter already solves?**
 We added AI to write code. Then AI to review code. Then AI to verify code. The thing that actually reduced incidents? ESLint.</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Fri, 12 Jun 2026 18:31:01 +0000</pubDate>
      <link>https://dev.to/utkarshbansal01/whats-the-most-over-engineered-ai-solution-youve-seen-for-a-problem-a-linter-already-48b5</link>
      <guid>https://dev.to/utkarshbansal01/whats-the-most-over-engineered-ai-solution-youve-seen-for-a-problem-a-linter-already-48b5</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://dev.to/utkarshbansal01/how-eslint-actually-works-the-quality-gate-behind-modern-javascript-50nk" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp4q01nv9t9kq2xj2trp6.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://dev.to/utkarshbansal01/how-eslint-actually-works-the-quality-gate-behind-modern-javascript-50nk" rel="noopener noreferrer" class="c-link"&gt;
            How ESLint Actually Works: The Quality Gate Behind Modern JavaScript - DEV Community
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            A few days ago, I shared an article:   You Don't Need Another Agent. You Need a Linter.   Then I did...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8j7kvp660rqzt99zui8e.png"&gt;
          dev.to
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>ai</category>
      <category>discuss</category>
      <category>javascript</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How ESLint Actually Works: The Quality Gate Behind Modern JavaScript</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Fri, 12 Jun 2026 12:37:30 +0000</pubDate>
      <link>https://dev.to/utkarshbansal01/how-eslint-actually-works-the-quality-gate-behind-modern-javascript-50nk</link>
      <guid>https://dev.to/utkarshbansal01/how-eslint-actually-works-the-quality-gate-behind-modern-javascript-50nk</guid>
      <description>&lt;p&gt;A few days ago, I shared an article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://dev.to/utkarsh_bansal_01/you-dont-need-another-agent-you-need-a-linter-1b9i"&gt;You Don't Need Another Agent. You Need a Linter.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I did what I do with anything I write: shared it around — a few publications, a few channels.&lt;/p&gt;

&lt;p&gt;Two reasons:&lt;/p&gt;

&lt;p&gt;First, feedback. I'd genuinely rather get roasted and fix my blind spots than stay comfortable and wrong.&lt;/p&gt;

&lt;p&gt;Second, let's be honest: reach.&lt;br&gt;
Every writer enjoys seeing a few more views.&lt;/p&gt;

&lt;p&gt;Most of the responses were positive.&lt;/p&gt;

&lt;p&gt;One wasn't.&lt;/p&gt;

&lt;p&gt;A publication rejected it with the reason:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;LOW_QUALITY&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair enough.&lt;/p&gt;

&lt;p&gt;It means there's room for improvement.&lt;/p&gt;

&lt;p&gt;Funny enough, my caffeinated 1 AM brain disagreed.&lt;/p&gt;

&lt;p&gt;Then it did what every developer does when someone says "this isn't good enough."&lt;/p&gt;

&lt;p&gt;It took that personally.&lt;/p&gt;

&lt;p&gt;So I went back and reread the article.&lt;/p&gt;

&lt;p&gt;And after the initial ego check, I realized something serious:&lt;/p&gt;

&lt;p&gt;The article talked in detail about ESLint, why it matters more in an AI-assisted world than ever.&lt;/p&gt;

&lt;p&gt;What it did not do was answer the question that actually matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is ESLint, how does it work, and why has half the JavaScript ecosystem quietly built its quality process around it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So let's fix that.&lt;/p&gt;

&lt;p&gt;Now, this isn't a sequel to my last piece about untangling vibe-coded code. It stands on its own — &lt;strong&gt;&lt;em&gt;one thing, done properly&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A complete teardown of ESLint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What it is&lt;/li&gt;
&lt;li&gt;How it works internally&lt;/li&gt;
&lt;li&gt;Why companies use it as a quality gate&lt;/li&gt;
&lt;li&gt;The different classes of problems it solves&lt;/li&gt;
&lt;li&gt;How plugins work&lt;/li&gt;
&lt;li&gt;How to write your own rules&lt;/li&gt;
&lt;li&gt;Where it fails&lt;/li&gt;
&lt;li&gt;Why it still beats many AI-based review systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fair warning.&lt;/p&gt;

&lt;p&gt;This article is going to be technical.&lt;/p&gt;

&lt;p&gt;There will be syntax trees.&lt;/p&gt;

&lt;p&gt;There will be compiler concepts.&lt;/p&gt;

&lt;p&gt;There will be enough JavaScript internals to make frontend developers slightly uncomfortable.&lt;/p&gt;

&lt;p&gt;I'll try my best to keep it readable not letting it turn into another manual - which nobody finishes.&lt;/p&gt;

&lt;p&gt;Let's start with the question most people never ask.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Is ESLint Actually Doing?
&lt;/h2&gt;

&lt;p&gt;Most developers describe ESLint like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It checks code for mistakes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Technically true. Also completely useless.&lt;/p&gt;

&lt;p&gt;That's like describing a car as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It helps you move.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The interesting part is &lt;strong&gt;&lt;em&gt;how?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider this code:&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="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&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;getUsers&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/users&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;With the right rules on, ESLint can tell us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;axios&lt;/code&gt; is imported and never used&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch&lt;/code&gt; is being used&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getUsers&lt;/code&gt; exists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getUsers&lt;/code&gt; is never called&lt;/li&gt;
&lt;li&gt;there are no syntax errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But how?&lt;/p&gt;

&lt;p&gt;The answer is that &lt;strong&gt;&lt;em&gt;ESLint never reads code as text&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The first thing it does is destroy your beautiful source code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Your Code Stops Being Code
&lt;/h2&gt;

&lt;p&gt;The moment ESLint sees a file, it passes it through a parser.&lt;/p&gt;

&lt;p&gt;The parser converts your code into something called an Abstract Syntax Tree (AST).&lt;/p&gt;

&lt;p&gt;Your code:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes something closer to:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VariableDeclaration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"const"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"declarations"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"answer"&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;"init"&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&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;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;Notice what's missing.&lt;/p&gt;

&lt;p&gt;Formatting. Spacing. Comments. Indentation.&lt;/p&gt;

&lt;p&gt;The parser doesn't care. The AST only cares about meaning.&lt;/p&gt;

&lt;p&gt;To ESLint, your source code isn't text anymore.&lt;/p&gt;

&lt;p&gt;It's a tree.&lt;/p&gt;

&lt;p&gt;Literally. That same &lt;code&gt;const answer = 42&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VariableDeclaration · const
└─ VariableDeclarator
   ├─ Identifier · "answer"
   └─ Literal · 42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON above and this tree are the same thing. One's just easier to look at.&lt;/p&gt;

&lt;p&gt;And every lint rule is basically a tree inspector.&lt;/p&gt;

&lt;p&gt;That's the core idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source code
   ↓
Parser
   ↓
AST
   ↓
Rules inspect nodes
   ↓
Reports / warnings / errors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Rules Walk The Tree
&lt;/h2&gt;

&lt;p&gt;Every ESLint rule is a visitor.&lt;/p&gt;

&lt;p&gt;It walks through the nodes inside the AST and asks questions.&lt;/p&gt;

&lt;p&gt;For example:&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;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="s2"&gt;debug&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;produces a node that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CallExpression
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;no-console&lt;/code&gt; rule simply visits every &lt;code&gt;CallExpression&lt;/code&gt; and asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this a console call?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If yes.&lt;/p&gt;

&lt;p&gt;Report error.&lt;/p&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;No AI. No embeddings. No vector database.&lt;/p&gt;

&lt;p&gt;Just deterministic tree traversal.&lt;/p&gt;

&lt;p&gt;Computer science doing computer science things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is More Powerful Than It Sounds
&lt;/h2&gt;

&lt;p&gt;Most people hear "static analysis" and immediately think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;So... it catches semicolons?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;Semicolons are the least interesting thing ESLint does.&lt;/p&gt;

&lt;p&gt;Let's look at the categories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Category 1: Correctness
&lt;/h3&gt;

&lt;p&gt;These rules catch code that is probably wrong.&lt;/p&gt;

&lt;p&gt;A function that doesn't exist:&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="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An assignment hiding inside a condition:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A promise nobody awaited:&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="nf"&gt;promiseFunction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are not style issues. These are bugs.&lt;/p&gt;

&lt;p&gt;The code may compile. It may even pass tests.&lt;/p&gt;

&lt;p&gt;But eventually somebody is getting paged at 2 AM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Category 2: Dead Code
&lt;/h3&gt;

&lt;p&gt;AI loves dead code. Humans create it too. AI just does it faster.&lt;/p&gt;

&lt;p&gt;An import that's never used:&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="nx"&gt;lodash&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lodash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A function nobody calls anymore:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;oldImplementation&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;One abandoned experiment later, and your repository becomes an archaeological site.&lt;/p&gt;

&lt;p&gt;Dead code increases bundle size, maintenance cost, and confusion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cheapest bug is the code that doesn't exist.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ESLint helps make that happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Category 3: Security
&lt;/h3&gt;

&lt;p&gt;This is where people start underestimating linters.&lt;/p&gt;

&lt;p&gt;Consider:&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="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No. Just no.&lt;/p&gt;

&lt;p&gt;Or:&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;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also no.&lt;/p&gt;

&lt;p&gt;A decent security-focused lint setup can stop entire classes of vulnerabilities before the code ever reaches production.&lt;/p&gt;

&lt;p&gt;Not because it's intelligent.&lt;/p&gt;

&lt;p&gt;Because it recognizes dangerous patterns.&lt;/p&gt;

&lt;p&gt;And dangerous patterns repeat. A lot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Category 4: Performance
&lt;/h3&gt;

&lt;p&gt;Some rules exist purely because developers keep making the same expensive mistakes.&lt;/p&gt;

&lt;p&gt;This:&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;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&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="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can become this:&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;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tiny improvement.&lt;/p&gt;

&lt;p&gt;Tiny improvement.&lt;/p&gt;

&lt;p&gt;Tiny improvement.&lt;/p&gt;

&lt;p&gt;Now repeat it across a million lines of code, and you've quietly compressed years of engineering experience into a config file.&lt;/p&gt;

&lt;p&gt;That's what lint rules really are.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Institutional memory&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Real Superpower: Quality Gates
&lt;/h2&gt;

&lt;p&gt;The biggest mistake teams make is treating ESLint like a suggestion.&lt;/p&gt;

&lt;p&gt;A linter running in your editor is nice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A linter blocking merges is transformative.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The moment ESLint becomes part of CI, it stops being advice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It becomes policy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The workflow changes from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hopefully somebody notices the bug in review.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code physically cannot merge.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a completely different game.&lt;/p&gt;

&lt;p&gt;Good engineering isn't about creating perfect developers.&lt;/p&gt;

&lt;p&gt;It's about creating systems where common mistakes cannot survive.&lt;/p&gt;

&lt;p&gt;ESLint is one of those systems.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer writes code
   ↓
Editor lint shows warnings
   ↓
PR created
   ↓
CI runs lint
   ↓
Pass → merge
Fail → fix before merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Community Plugins Already Solved Half Your Problems
&lt;/h2&gt;

&lt;p&gt;Here's the part most developers miss. &lt;/p&gt;

&lt;p&gt;ESLint is a rule engine.&lt;/p&gt;

&lt;p&gt;It parses code, walks the AST, runs rules, reports violations, and that's basically the entire core.&lt;/p&gt;

&lt;p&gt;The real power comes from everything built on top of it: plugins, shared rule sets, and years of accumulated engineering knowledge.&lt;/p&gt;

&lt;p&gt;Need to find unused imports?&lt;/p&gt;

&lt;p&gt;There's a plugin. &lt;code&gt;eslint-plugin-unused-imports&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Need TypeScript-aware analysis?&lt;/p&gt;

&lt;p&gt;There's a plugin. &lt;code&gt;@typescript-eslint&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Need accessibility checks for your components?&lt;/p&gt;

&lt;p&gt;There's a plugin. &lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt; or &lt;code&gt;eslint-plugin-vuejs-accessibility&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Need to catch dangerous security patterns?&lt;/p&gt;

&lt;p&gt;There's a plugin. &lt;code&gt;eslint-plugin-security&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Need to enforce architectural boundaries between layers?&lt;/p&gt;

&lt;p&gt;Believe it or not, there's a plugin for that too. &lt;code&gt;eslint-plugin-boundaries.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You don't have to write any of this yourself.&lt;/p&gt;

&lt;p&gt;Thousands of developers have already spent years turning common mistakes into reusable rules.&lt;/p&gt;

&lt;p&gt;A few common examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;eslint-plugin-unused-imports&lt;/code&gt; — dead imports and variables&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eslint-plugin-import&lt;/code&gt; — circular dependencies and broken paths&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@typescript-eslint&lt;/code&gt; — type-aware correctness rules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eslint-plugin-jsx-a11y&lt;/code&gt; / &lt;code&gt;eslint-plugin-vuejs-accessibility&lt;/code&gt; — accessibility&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eslint-plugin-promise&lt;/code&gt; — dropped awaits and async mistakes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eslint-plugin-security&lt;/code&gt; — dangerous patterns like &lt;code&gt;eval&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eslint-plugin-boundaries&lt;/code&gt; — architectural layer rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the mental shift.&lt;/p&gt;

&lt;p&gt;Most people think ESLint is one tool with a bunch of built-in rules.&lt;/p&gt;

&lt;p&gt;It's closer to a platform.&lt;/p&gt;

&lt;p&gt;A small, stable engine that lets an entire ecosystem contribute knowledge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  eslint-plugin-import     @typescript-eslint     your local rules
   (cycles, dead code)     (type-aware checks)     (team gates)
            │                      │                      │
            └──────────────────────┼──────────────────────┘
                                   ▼
            ┌──────────────────────────────────────────┐
            │                ESLint core                 │
            │     parse → walk the AST → run → report     │
            └──────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's why ESLint has survived for so long.&lt;/p&gt;

&lt;p&gt;The core doesn't need to know about &lt;code&gt;React&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or &lt;code&gt;Vue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or &lt;code&gt;TypeScript&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or whatever framework we'll all be rewriting our applications in next year.&lt;/p&gt;

&lt;p&gt;The engine stays the same.&lt;/p&gt;

&lt;p&gt;The ecosystem keeps growing.&lt;/p&gt;

&lt;p&gt;The core never changes. The rules feeding into it are infinite.&lt;/p&gt;

&lt;p&gt;And eventually you realize something interesting:&lt;/p&gt;

&lt;p&gt;Most teams don't need more tooling.&lt;/p&gt;

&lt;p&gt;They just need to enable the plugins that already exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  When The Defaults Aren't Enough: Write Your Own Rule
&lt;/h2&gt;

&lt;p&gt;Now for the fun part — the part that makes ESLint feel less like a tool and more like a system.&lt;/p&gt;

&lt;p&gt;Suppose your company has a design system. And somebody keeps committing raw colors:&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;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#FF0000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&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;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can complain. You can review every PR. Or you can automate the complaint.&lt;/p&gt;

&lt;p&gt;Here's the entire rule:&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;// rules/no-raw-color.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&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="s2"&gt;problem&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rawColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use a design token, not a raw hex like '{{value}}'.&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="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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="c1"&gt;// the walker hands you every string literal; you keep the ones that look like hex&lt;/span&gt;
      &lt;span class="nc"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/^#&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{3,6}&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rawColor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;node&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;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="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;That's it. No npm package, no publishing. Flat config lets you wire a local rule in directly, and ESLint's built-in &lt;code&gt;RuleTester&lt;/code&gt; lets you prove it fires on &lt;code&gt;"#FF0000"&lt;/code&gt; and stays quiet on &lt;code&gt;tokens.error&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A custom ESLint rule is usually less code than the Slack argument it prevents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the real secret.&lt;/p&gt;

&lt;p&gt;The best engineering teams don't repeatedly solve the same problem.&lt;/p&gt;

&lt;p&gt;They automate it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every bug becomes a rule. Every rule becomes institutional knowledge. Every future developer benefits.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Human. Or AI. Doesn't matter.&lt;/p&gt;

&lt;p&gt;The gate treats everybody equally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where ESLint Fails
&lt;/h2&gt;

&lt;p&gt;Now, before somebody accuses me of turning ESLint into a religion — it has limits.&lt;/p&gt;

&lt;p&gt;ESLint cannot tell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if your architecture is good&lt;/li&gt;
&lt;li&gt;if your business logic is correct&lt;/li&gt;
&lt;li&gt;if the feature should even exist&lt;/li&gt;
&lt;li&gt;if your product manager had a terrible idea&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those remain human problems.&lt;/p&gt;

&lt;p&gt;Static analysis sees structure. Not intent.&lt;/p&gt;

&lt;p&gt;A green lint report does not mean correct software.&lt;/p&gt;

&lt;p&gt;It means your code survived a specific set of inspections.&lt;/p&gt;

&lt;p&gt;Nothing more. Nothing less.&lt;/p&gt;

&lt;h2&gt;
  
  
  So Why Am I Still Talking About ESLint In The Age Of AI?
&lt;/h2&gt;

&lt;p&gt;Because every week I see somebody proposing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI review agents&lt;/li&gt;
&lt;li&gt;AI architecture agents&lt;/li&gt;
&lt;li&gt;AI quality agents&lt;/li&gt;
&lt;li&gt;AI code verification agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And half the time, they're trying to solve problems ESLint solved years ago.&lt;/p&gt;

&lt;p&gt;Before building another agent, ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can a deterministic static analysis tool already solve this?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because if the answer is yes, the boring tool is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;faster&lt;/li&gt;
&lt;li&gt;cheaper&lt;/li&gt;
&lt;li&gt;reproducible&lt;/li&gt;
&lt;li&gt;and it doesn't hallucinate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The boring solution often wins.&lt;/p&gt;

&lt;p&gt;Not because it's exciting. Because it works.&lt;/p&gt;

&lt;p&gt;And software engineering eventually rewards whatever keeps production alive — even if it was invented twenty years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The most important thing I learned from AI-assisted development wasn't how powerful AI is.&lt;/p&gt;

&lt;p&gt;It was how valuable boring tooling remains.&lt;/p&gt;

&lt;p&gt;ESLint isn't glamorous. Nobody posts screenshots of lint checks on LinkedIn. Nobody raises venture capital for a missing-semicolon detector.&lt;/p&gt;

&lt;p&gt;But every large JavaScript codebase eventually discovers the same truth.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It parses code. It walks trees. It applies rules. It reports violations. That sounds humble — but the impact compounds: it catches bugs early, enforces standards, protects architecture, scales institutional memory, and keeps bad code from sneaking through the door.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And unlike the rest of us, it never gets tired.&lt;/p&gt;

&lt;p&gt;The cheapest reviewer on your team is still a linter.&lt;/p&gt;

&lt;p&gt;If you still think of ESLint as a semicolon cop, you're missing most of the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It was never about the semicolons.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>architecture</category>
      <category>learning</category>
    </item>
    <item>
      <title>What's the most over-engineered AI solution you've seen for a problem a linter already solves?
We added AI to write code. Then AI to review code. Then AI to verify code.
The thing that actually reduced incidents? ESLint.</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Wed, 10 Jun 2026 15:54:26 +0000</pubDate>
      <link>https://dev.to/utkarshbansal01/whats-the-most-over-engineered-ai-solution-youve-seen-for-a-problem-a-linter-already-solves-we-1o55</link>
      <guid>https://dev.to/utkarshbansal01/whats-the-most-over-engineered-ai-solution-youve-seen-for-a-problem-a-linter-already-solves-we-1o55</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://dev.to/utkarshbansal01/you-dont-need-another-agent-you-need-a-linter-1b9i" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8j3zlt8lcf5gzmdmvsx.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://dev.to/utkarshbansal01/you-dont-need-another-agent-you-need-a-linter-1b9i" rel="noopener noreferrer" class="c-link"&gt;
            You Don't Need Another Agent. You Need a Linter. - DEV Community
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            In my last post I complained — a lot — about vibe code and the beautiful disaster it left...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8j7kvp660rqzt99zui8e.png"&gt;
          dev.to
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>ai</category>
      <category>discuss</category>
      <category>javascript</category>
      <category>tooling</category>
    </item>
    <item>
      <title>You Don't Need Another Agent. You Need a Linter.</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Wed, 10 Jun 2026 15:49:20 +0000</pubDate>
      <link>https://dev.to/utkarshbansal01/you-dont-need-another-agent-you-need-a-linter-1b9i</link>
      <guid>https://dev.to/utkarshbansal01/you-dont-need-another-agent-you-need-a-linter-1b9i</guid>
      <description>&lt;p&gt;In my last post I complained — a lot — about vibe code and the beautiful disaster it left behind.&lt;/p&gt;

&lt;p&gt;Now, I'm not here just to complain. There were a lot of learning opportunities too, like how to handle legacy / vibe code. Because at the end of the day, both are the same: no one knows how they work, but somehow they keep working. Touching them is like defusing a bomb — you never know how your change might cascade and break the core logic.&lt;/p&gt;

&lt;p&gt;The good news is that vibe code is much simpler than legacy. AI, in all its glory, tries to write perfect-looking code — proper function names, comments, the works — not like legacy code where a single function runs 500 lines, with spaghetti names all over that make no sense and comments that are out of date.&lt;/p&gt;

&lt;p&gt;And that makes it something I can actually handle. I still don't have a perfect, step-by-step playbook — but I've got pieces.&lt;/p&gt;

&lt;p&gt;The first one. The cheapest and the oldest one. The one the industry solved decades ago and the whole "AI built my app in a day" crowd somehow forgot exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A linter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, you heard me right. A linter. ESLint.&lt;/p&gt;

&lt;p&gt;Most people who've been in this industry already know it. It's the most boring, reliable tool in the box.&lt;/p&gt;

&lt;p&gt;But in an era where the answer to every problem is "add another AI," it's worth saying out loud why the boring tool still wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a linter actually is
&lt;/h2&gt;

&lt;p&gt;If you vibe-coded your way into this world, or you're new to web dev in general and have never heard the word "lint", here's the honest version.&lt;/p&gt;

&lt;p&gt;A linter is a set of rules you add to your repo. It reads your code without running it, checks it against those rules, and flags everything that's broken, sloppy, or about to bite you in production.&lt;/p&gt;

&lt;p&gt;The detail people get wrong: it's not a &lt;code&gt;grep&lt;/code&gt; for bad words.&lt;/p&gt;

&lt;p&gt;A real linter parses your code into a syntax tree and actually reasons about its structure — what's imported, what's called, what's reachable, what types flow where.&lt;/p&gt;

&lt;p&gt;That's why a rule like &lt;code&gt;no-unused-vars&lt;/code&gt; can tell "you imported this and never used it" apart from "you used this without importing it."&lt;/p&gt;

&lt;p&gt;It's also why &lt;code&gt;eslint-plugin-unicorn&lt;/code&gt; can quietly rewrite &lt;code&gt;array.indexOf(x) !== -1&lt;/code&gt; into &lt;code&gt;array.includes(x)&lt;/code&gt; (&lt;code&gt;unicorn/prefer-includes&lt;/code&gt;), or flag a &lt;code&gt;new Array(...)&lt;/code&gt; call that's going to bite you — without ever executing a single line.&lt;/p&gt;

&lt;p&gt;Three properties matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't run your code.&lt;/strong&gt; No side effects, no flaky network, no "works on my machine."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's deterministic.&lt;/strong&gt; Same code in, same result out, every single time. No temperature, no vibes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing.&lt;/strong&gt; No tokens. No API bill. It runs in seconds on a laptop or a CI box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2026, that last one is the whole argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it used to
&lt;/h2&gt;

&lt;p&gt;We have AI to write code. AI to review code. AI to verify code. Agents stacked on agents, each one quietly spending tokens to do something a free tool already did better.&lt;/p&gt;

&lt;p&gt;And honestly? Most of those agents are re-solving problems that were solved long before "agent" meant what it means today.&lt;/p&gt;

&lt;p&gt;You do not need a language model to notice that you left a debug log in production (&lt;code&gt;no-console&lt;/code&gt;). Or imported a module you never used (&lt;code&gt;unused-imports/no-unused-imports&lt;/code&gt;). Or wrote a function nobody calls (&lt;code&gt;knip&lt;/code&gt;). Or forgot to &lt;code&gt;await&lt;/code&gt; a promise (&lt;code&gt;@typescript-eslint/no-floating-promises&lt;/code&gt;). Or made two files import each other in a circle (&lt;code&gt;import/no-cycle&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A linter catches every one of those instantly, deterministically, for free — before any AI even wakes up.&lt;/p&gt;

&lt;p&gt;So before you bolt another agent onto your pipeline, ask the boring question: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does the free, battle-tested tool already do this?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Usually it does.&lt;/p&gt;

&lt;p&gt;Use what already exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  A linter is a &lt;strong&gt;Quality Gate&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the real reason linters matter. Whether you're a big company or a freelancer just starting out.&lt;/p&gt;

&lt;p&gt;A linter is a quality gate. A line in the sand that says: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This code does not get in unless it clears the bar.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Where I work, we wired this straight into the pull-request flow. A check a PR &lt;em&gt;has&lt;/em&gt; to pass before it can merge.&lt;/p&gt;

&lt;p&gt;Not a suggestion. Not "we'll clean it up later." A hard gate on the merge button.&lt;/p&gt;

&lt;p&gt;The result was the kind of number that sounds made up until you live it: production issues and bad merges dropped by roughly 80%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magic?&lt;/strong&gt; No.&lt;/p&gt;

&lt;p&gt;Most of our incidents were the same boring handful — an unhandled promise that swallowed an error, a stray debug log left in, a circular import that only broke in prod. The gate caught every one of them at the door — we just stopped letting broken code through.&lt;/p&gt;

&lt;p&gt;That's the whole trick. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A gate doesn't make anyone smarter. It just raises the floor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A tip if you're at a big company: when you introduce the ESLint check, you'll hear a lot of pushback from devs — especially at a startup or midsize company. So be ready for a fight, and roll it out gradually: a warning gate first, then an error gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  It doesn't stop at "missing semicolon"
&lt;/h2&gt;

&lt;p&gt;Here's the common misconception people hit when they first add ESLint and see all those errors: "this thing is bad, it just nags me like my annoying brother." That's a huge underestimation.&lt;/p&gt;

&lt;p&gt;Once you treat the linter as a real gate instead of a nag, you can teach it your specific mistakes. And it never forgets them.&lt;/p&gt;

&lt;p&gt;Every painful bug in an AI-assisted codebase is also a &lt;em&gt;pattern&lt;/em&gt;. And patterns are exactly what static analysis is good at.&lt;/p&gt;

&lt;p&gt;A surprising amount of this is one config file away. Modern ESLint flat config plus a few community plugins covers most of it:&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;// eslint.config.js — a starter "quality gate", not a missing-semicolon nag&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;unusedImports&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eslint-plugin-unused-imports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;importPlugin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eslint-plugin-import&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;unicorn&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eslint-plugin-unicorn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;plugins&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="s2"&gt;unused-imports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unusedImports&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;import&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;importPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unicorn&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;languageOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// type-aware rules need the TS program, not just the syntax tree&lt;/span&gt;
      &lt;span class="na"&gt;parserOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projectService&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="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// ~100 perf + hidden-bug rules in one line; downgrade the churny ones&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;unicorn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flat/recommended&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unicorn/filename-case&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="s2"&gt;off&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Vue files are PascalCase by convention&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unicorn/prevent-abbreviations&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="s2"&gt;off&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// trips on `ref`, `props`, store names&lt;/span&gt;
      &lt;span class="c1"&gt;// dead code the model left behind — auto-fixable&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unused-imports/no-unused-imports&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="s2"&gt;error&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="s2"&gt;unused-imports/no-unused-vars&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="s2"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// the silent killer (see the circular-deps post) — crank maxDepth up&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;import/no-cycle&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&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;maxDepth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ignoreExternal&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;// dropped awaits that swallow errors — needs the type info above&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@typescript-eslint/no-floating-promises&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="s2"&gt;error&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="s2"&gt;@typescript-eslint/no-misused-promises&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="s2"&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;// dynamic code execution: off by default in eslint:recommended, turn them on&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-eval&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="s2"&gt;error&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="s2"&gt;no-implied-eval&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="s2"&gt;error&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="s2"&gt;no-new-func&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="s2"&gt;error&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="s2"&gt;no-script-url&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="s2"&gt;error&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;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the floor.&lt;/p&gt;

&lt;p&gt;On top of it, every time something hurt us, we added a rule so it could only hurt us once. Three of them earned their place the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Circular dependencies&lt;/strong&gt; — &lt;code&gt;import/no-cycle&lt;/code&gt; with &lt;code&gt;maxDepth: 10&lt;/code&gt; (the default of &lt;code&gt;1&lt;/code&gt; only sees direct A↔B cycles), backed by &lt;code&gt;dependency-cruiser&lt;/code&gt; for the whole-graph view. AI refactors &lt;em&gt;love&lt;/em&gt; making two modules import each other until the build breaks only in prod. It's the single most common AI-induced bug I've seen — enough that it deserves a post of its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropped awaits&lt;/strong&gt; — &lt;code&gt;@typescript-eslint/no-floating-promises&lt;/code&gt; and &lt;code&gt;no-misused-promises&lt;/code&gt;. A missing &lt;code&gt;await&lt;/code&gt; on a file or network call silently swallows the error, and you find out when a user does. Needs &lt;em&gt;type-aware&lt;/em&gt; linting (&lt;code&gt;parserOptions.projectService&lt;/code&gt;) — the one setup cost worth paying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The free-wins pack&lt;/strong&gt; — &lt;code&gt;eslint-plugin-unicorn&lt;/code&gt;'s &lt;code&gt;flat/recommended&lt;/code&gt;: ~100 rules for one import. A mix of performance wins (&lt;code&gt;prefer-includes&lt;/code&gt;, &lt;code&gt;prefer-set-has&lt;/code&gt;), real hidden-bug catches (&lt;code&gt;no-array-push-push&lt;/code&gt;, &lt;code&gt;no-thenable&lt;/code&gt;), and some opinionated style. Turn off the churn (&lt;code&gt;unicorn/filename-case&lt;/code&gt;, &lt;code&gt;unicorn/prevent-abbreviations&lt;/code&gt;, &lt;code&gt;unicorn/no-null&lt;/code&gt;) and keep the rest. Best caught-mistakes-to-config ratio you'll find.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest are quieter, but each one caught something once. Skim them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dead code&lt;/strong&gt; — &lt;code&gt;eslint-plugin-unused-imports&lt;/code&gt; sweeps the imports the model added for an approach it then abandoned (auto-fixed on &lt;code&gt;--fix&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;eval&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;no-eval&lt;/code&gt;, &lt;code&gt;no-implied-eval&lt;/code&gt;, &lt;code&gt;no-new-func&lt;/code&gt;, &lt;code&gt;no-script-url&lt;/code&gt;. Off by default; zero legit uses, infinite ways for generated code to sneak one in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSS sinks&lt;/strong&gt; — &lt;code&gt;vue/no-v-html&lt;/code&gt; / &lt;code&gt;react/no-danger&lt;/code&gt;, plus &lt;code&gt;eslint-plugin-no-unsanitized&lt;/code&gt; for raw &lt;code&gt;innerHTML&lt;/code&gt;. Any HTML injection has to pass through a sanitizer, enforced at lint time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded strings&lt;/strong&gt; — &lt;code&gt;eslint-plugin-i18next&lt;/code&gt;, so the day you add a language nothing's baked into the markup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test hygiene&lt;/strong&gt; — &lt;code&gt;no-focused-tests&lt;/code&gt; as an &lt;strong&gt;error&lt;/strong&gt;, so a stray &lt;code&gt;it.only&lt;/code&gt; can't silently shrink the CI suite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaked secrets&lt;/strong&gt; — &lt;code&gt;eslint-plugin-no-secrets&lt;/code&gt; flags high-entropy literals that look like committed tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design drift&lt;/strong&gt; (custom) — a ~40-line AST script that fails the build when a raw hex/size/shadow shows up where a named token should be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer boundaries&lt;/strong&gt; — &lt;code&gt;eslint-plugin-boundaries&lt;/code&gt; to stop one layer importing another, plus a custom rule forcing risky mutations through a single function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is a lesson we only had to learn once.&lt;/p&gt;

&lt;p&gt;After that, the linter remembers it for us. Forever, for free, on every commit, for every contributor — human or AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick that keeps the gate from being annoying
&lt;/h2&gt;

&lt;p&gt;Most people give up on linting for one reason. They turn on every rule at &lt;code&gt;error&lt;/code&gt; on day one, drown in ten thousand warnings, and rage-quit.&lt;/p&gt;

&lt;p&gt;Don't do that.&lt;/p&gt;

&lt;p&gt;Run new rules &lt;strong&gt;warn-first&lt;/strong&gt;. Let them report without blocking. Get the count to zero on your own schedule. &lt;em&gt;Then&lt;/em&gt; ratchet that rule up to &lt;code&gt;error&lt;/code&gt; so it can never regress.&lt;/p&gt;

&lt;p&gt;Here's the reframe that matters: those warnings aren't a to-do list you owe the linter today. They're a map of where the mines are buried. You freeze the count, stop anyone from planting new ones, and clear the field at your own pace.&lt;/p&gt;

&lt;p&gt;For the handful of legacy offenders you can't fix today, keep an explicit allowlist. And treat adding to that allowlist as the thing you're not allowed to do.&lt;/p&gt;

&lt;p&gt;The list only ever shrinks.&lt;/p&gt;

&lt;p&gt;That's how a gate stays strict without crying wolf — and how you tame inherited code without it blowing up in your face.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A linter only sees what's static.&lt;/strong&gt; Dynamic imports, string-built code, and values resolved at runtime are invisible to it. Know the blind spots. A green lint is not "provably correct."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--fix&lt;/code&gt; is not free thinking.&lt;/strong&gt; Auto-fix is great for unused imports and formatting. It is not great at deciding whether a flagged thing was a real mistake. Read the diff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate on the trend, not the absolute count.&lt;/strong&gt; Failing CI because the repo has &lt;em&gt;any&lt;/em&gt; warnings is untenable in a vibe-coded codebase. Fail when the count grows past the baseline. That's actionable. "Be perfect" isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rule you suppress everywhere is a rule you deleted.&lt;/strong&gt; If every other line has a disable comment, you didn't pass the gate — you removed it. Fix the code or kill the rule honestly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The mild jab
&lt;/h2&gt;

&lt;p&gt;Here's the irony I can already see coming. You've read this whole piece on why bolting AI onto already-solved problems is wasteful — and your very next move is to open a chat window and type &lt;em&gt;"add ESLint and all the plugins to my repo."&lt;/em&gt; Or worse: &lt;em&gt;"build me a code-review agent that flags unused imports and circular deps."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Stop.&lt;/p&gt;

&lt;p&gt;Both are the wrong move. The first one drops you into a rabbit hole of 1000+ errors you'll be digging out of forever. Start with the base recommendation, then slowly work toward the rest.&lt;/p&gt;

&lt;p&gt;As for the code-review agent — I'll pretend I didn't hear that. We just spent this whole article on why a linter beats an AI reviewer here. So why are you still reaching for it? Look, if you hate your tokens that much — if you're really itching to spend them on a problem a free, decades-old tool already solves &lt;em&gt;deterministically&lt;/em&gt; — just send them my way. I'll put them to better use.&lt;/p&gt;

&lt;p&gt;AI is genuinely good at the fuzzy stuff — turning an idea into reality, that dopamine hit of making something from nothing. Let it do that. Leave the heavy judgement of code quality to linter.&lt;/p&gt;

&lt;p&gt;And let me be honest about the other side: a linter won't fix your architecture, won't catch a wrong business rule, won't tell you the feature was a bad idea. That part is still on you. What it &lt;em&gt;will&lt;/em&gt; do is take the boring, mechanical, never-ending category of mistakes off your plate — so your brain is free for the part that actually needs one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A linter is the cheapest reviewer on your team. Free, deterministic, tireless, and completely unimpressed by hype.&lt;/p&gt;

&lt;p&gt;Make it a hard gate on merge, and you raise the floor for everyone at once.&lt;/p&gt;

&lt;p&gt;Then teach it your own recurring mistakes — token drift, hardcoded strings, circular deps, unsafe HTML, dropped awaits — one custom rule per bug class, warn-first then ratcheted to error.&lt;/p&gt;

&lt;p&gt;Add the linter first.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Then&lt;/em&gt;, if you still want an agent, at least it's reviewing code that already cleared the bar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cheapest reviewer you'll ever hire has been sitting in your repo the whole time.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Frontend Is Not That Simple</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Thu, 21 May 2026 19:15:01 +0000</pubDate>
      <link>https://dev.to/utkarshbansal01/frontend-is-not-that-simple-199n</link>
      <guid>https://dev.to/utkarshbansal01/frontend-is-not-that-simple-199n</guid>
      <description>&lt;p&gt;I guess most of you already have a pretty basic idea of what this post is about — Frontend Engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why frontend?&lt;/strong&gt;&lt;br&gt;
“We have AI now. It can create a website in a day. Why do you even care about frontend anymore? Post something about AI blah blah blah…”&lt;/p&gt;

&lt;p&gt;I hear you.&lt;/p&gt;

&lt;p&gt;But the real question is: &lt;strong&gt;Do you actually understand frontend?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every day, I see product managers and designers trying their hands at AI — creating MVPs in a day and celebrating like, “We don’t need engineers anymore. We can do everything ourselves now.”&lt;/p&gt;

&lt;p&gt;And honestly? That part is impressive.&lt;/p&gt;

&lt;p&gt;But then that same code gets handed over to engineers for optimization, scalability, maintainability, performance, accessibility, security, deployment, and production readiness.&lt;/p&gt;

&lt;p&gt;And that’s where my nightmare begins.&lt;/p&gt;

&lt;p&gt;AI generates code based on patterns and existing code available online. It doesn’t magically create great architecture, scalability, performance, accessibility, or production-grade systems out of thin air.&lt;/p&gt;

&lt;p&gt;The bigger problem starts when people continuously edit AI-generated code using more AI prompts without understanding the underlying structure.&lt;/p&gt;

&lt;p&gt;At first, it works.&lt;/p&gt;

&lt;p&gt;Then slowly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;functions start leaking across modules&lt;/li&gt;
&lt;li&gt;components become tightly coupled&lt;/li&gt;
&lt;li&gt;business logic spreads everywhere&lt;/li&gt;
&lt;li&gt;state management becomes unpredictable&lt;/li&gt;
&lt;li&gt;abstractions collapse&lt;/li&gt;
&lt;li&gt;duplication increases&lt;/li&gt;
&lt;li&gt;and eventually the entire codebase turns into spaghetti&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tangled mess held together by vibes, Stack Overflow answers, and “it works on my machine.”&lt;/p&gt;

&lt;p&gt;Then comes the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;“Why hasn’t the app that was created in a day scaled and deployed yet?”&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So for the sake of my sanity, let me take you on a small journey into what real frontend engineering actually looks like.&lt;/p&gt;

&lt;p&gt;And do note — this is just the tip of the iceberg. Once you go deeper, it becomes an abyss.&lt;/p&gt;




&lt;h3&gt;
  
  
  Frontend Engineering
&lt;/h3&gt;

&lt;p&gt;As per product teams:&lt;br&gt;
&lt;em&gt;“Someone who creates beautiful websites using the design team’s instructions.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As per textbooks:&lt;br&gt;
&lt;em&gt;“HTML, CSS, and JavaScript.”&lt;/em&gt;&lt;br&gt;
HTML is the skeleton, CSS is the skin, and JavaScript is the brain.&lt;/p&gt;

&lt;p&gt;As per me?&lt;br&gt;
It’s much more than that.&lt;/p&gt;




&lt;p&gt;Frontend Engineering&lt;br&gt;
├── &lt;strong&gt;UI Development&lt;/strong&gt; — What everyone sees&lt;br&gt;
├── &lt;strong&gt;State Management&lt;/strong&gt; — What no one sees until it breaks&lt;br&gt;
├── &lt;strong&gt;Routing&lt;/strong&gt; — Mostly ignored, but critical unless you enjoy navigation nightmares&lt;br&gt;
├── &lt;strong&gt;Design Systems&lt;/strong&gt; — Design tokens keep the UI consistent&lt;br&gt;
├── &lt;strong&gt;API/Data Layer&lt;/strong&gt; — Most production issues start here&lt;br&gt;
├── &lt;strong&gt;Authentication&lt;/strong&gt; — One of the biggest production concerns&lt;br&gt;
├── &lt;strong&gt;Performance&lt;/strong&gt; — In production, every millisecond matters&lt;br&gt;
├── &lt;strong&gt;Accessibility&lt;/strong&gt; — Most developers ignore this until they shouldn’t&lt;br&gt;
├── &lt;strong&gt;SEO&lt;/strong&gt; — Decides whether people even discover your product&lt;br&gt;
├── &lt;strong&gt;Security&lt;/strong&gt; — Skip this and enjoy your legal/security nightmare&lt;br&gt;
├── &lt;strong&gt;Build Tooling&lt;/strong&gt; — A massive game changer if you truly understand it&lt;br&gt;
├── &lt;strong&gt;Testing&lt;/strong&gt; — Protects both your product and your sleep&lt;br&gt;
├── &lt;strong&gt;Monitoring&lt;/strong&gt; — Users will always find bugs in ways you never expected&lt;br&gt;
├── &lt;strong&gt;Analytics&lt;/strong&gt; — So you can actually measure improvements instead of guessing&lt;br&gt;
├── &lt;strong&gt;Deployment&lt;/strong&gt; — Because shipping localhost to customers isn’t a strategy&lt;br&gt;
├── &lt;strong&gt;CI/CD&lt;/strong&gt; — Automating releases so deployments don’t become rituals&lt;br&gt;
├── &lt;strong&gt;Internationalization&lt;/strong&gt; — Your app eventually speaks more than one language&lt;br&gt;
├── &lt;strong&gt;Offline/PWA&lt;/strong&gt; — Because internet connections aren’t always reliable&lt;br&gt;
├── &lt;strong&gt;Realtime Systems&lt;/strong&gt; — Notifications, chats, collaboration, live updates&lt;br&gt;
├── &lt;strong&gt;Architecture&lt;/strong&gt; — The difference between scaling smoothly and suffering later&lt;br&gt;
└── &lt;strong&gt;Developer Experience&lt;/strong&gt; — Better DX = faster teams and fewer headaches&lt;/p&gt;




&lt;p&gt;Now some people will say:&lt;br&gt;
&lt;em&gt;“Come on, this is an over-the-top explanation.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And honestly?&lt;br&gt;
They’re not completely wrong.&lt;/p&gt;

&lt;p&gt;But it’s also true that all of these areas fall under frontend engineering in modern production systems.&lt;/p&gt;

&lt;p&gt;Now the next question is:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Do we really need all of this?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most of the time? Probably not.&lt;/p&gt;

&lt;p&gt;Modern frameworks already provide many things out of the box.&lt;br&gt;
For example, when you create a Vue project today, you already get tools like Vite, package managers like npm/pnpm/yarn, routing setups, build pipelines, and much more.&lt;/p&gt;

&lt;p&gt;But if you want to build something truly great — something scalable, performant, maintainable, and production-ready — eventually you’ll have to understand these layers.&lt;/p&gt;

&lt;p&gt;That’s where frontend engineering actually begins.&lt;/p&gt;




&lt;p&gt;And if you’re wondering what the “abyss” looks like…&lt;/p&gt;

&lt;p&gt;It looks something like this:&lt;/p&gt;

&lt;p&gt;Frontend Engineering&lt;br&gt;
├── Rendering Strategies (CSR, SSR, SSG, ISR, Streaming)&lt;br&gt;
├── Hydration &amp;amp; Partial Hydration&lt;br&gt;
├── Reactivity Systems &amp;amp; Virtual DOM internals&lt;br&gt;
├── Compiler Architecture&lt;br&gt;
├── Browser Rendering Pipeline&lt;br&gt;
├── GPU Acceleration &amp;amp; Paint Optimization&lt;br&gt;
├── Edge Computing&lt;br&gt;
├── Caching Layers&lt;br&gt;
├── WebSockets &amp;amp; CRDTs&lt;br&gt;
├── Distributed State Synchronization&lt;br&gt;
├── Micro Frontends&lt;br&gt;
├── Monorepo Architecture&lt;br&gt;
├── Design Token Pipelines&lt;br&gt;
├── Runtime Performance Profiling&lt;br&gt;
├── Web Security Hardening&lt;br&gt;
├── Memory Leak Debugging&lt;br&gt;
├── Service Workers&lt;br&gt;
├── Rendering at Scale&lt;br&gt;
├── Bundle Optimization&lt;br&gt;
├── Build System Internals&lt;br&gt;
├── Accessibility Compliance&lt;br&gt;
├── Observability Engineering&lt;br&gt;
├── Frontend Infrastructure&lt;br&gt;
├── Server Components&lt;br&gt;
├── WASM Integration&lt;br&gt;
├── Compiler Toolchains&lt;br&gt;
└── Browser Engine Edge Cases&lt;/p&gt;

&lt;p&gt;And somewhere in between all this, you realize:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Frontend engineering was never just about making buttons look pretty.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Article 3 of 100: The PR Crisis: GIT/GitHub Commits Cheat Sheet — A Developer’s Redemption Arc</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Sun, 02 Mar 2025 14:39:22 +0000</pubDate>
      <link>https://dev.to/utkarshbansal01/article-3-of-100-the-pr-crisis-gitgithub-commits-cheat-sheet-a-developers-redemption-arc-2d1b</link>
      <guid>https://dev.to/utkarshbansal01/article-3-of-100-the-pr-crisis-gitgithub-commits-cheat-sheet-a-developers-redemption-arc-2d1b</guid>
      <description>&lt;p&gt;Handling Git mistakes is a crucial skill for any developer. Recently, I found myself in a situation where a simple feature branch turned into a mess—too many commits, merge conflicts, and unclear history. Instead of starting over, I took the time to properly fix it.&lt;/p&gt;

&lt;p&gt;Here’s what helped:&lt;br&gt;
✔️ git reset — Undo unnecessary commits&lt;br&gt;
✔️ git cherry-pick — Apply specific changes cleanly&lt;br&gt;
✔️ git reflog — Recover lost commits&lt;br&gt;
✔️ git stash — Save work without committing&lt;/p&gt;

&lt;p&gt;Learning to manage Git efficiently makes PRs easier to review and keeps repositories clean.&lt;/p&gt;

&lt;p&gt;If you’ve ever struggled with a tangled Git history, this article might help.&lt;/p&gt;

&lt;p&gt;📖 Read more: &lt;a href="https://medium.com/p/19c1f22e29ed" rel="noopener noreferrer"&gt;https://medium.com/p/19c1f22e29ed&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>git</category>
      <category>cheatsheet</category>
      <category>developer</category>
    </item>
    <item>
      <title>Day 3/100: The PR Crisis: GIT/GitHub Commits Cheat Sheet — A Developer’s Redemption Arc Start here: https://medium.com/p/19c1f22e29ed</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Sun, 02 Mar 2025 14:35:50 +0000</pubDate>
      <link>https://dev.to/utkarshbansal01/day-3100-the-pr-crisis-gitgithub-commits-cheat-sheet-a-developers-redemption-arc-start-3e4h</link>
      <guid>https://dev.to/utkarshbansal01/day-3100-the-pr-crisis-gitgithub-commits-cheat-sheet-a-developers-redemption-arc-start-3e4h</guid>
      <description></description>
    </item>
    <item>
      <title>Day 2/100: ‘Wait, don’t we already have this function?’ — Publishing Your First NPM Package 📦</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Sat, 15 Feb 2025 17:23:41 +0000</pubDate>
      <link>https://dev.to/utkarsh_bansal_1ea593e019/day-2100-wait-dont-we-already-have-this-function-publishing-your-first-npm-package-5dce</link>
      <guid>https://dev.to/utkarsh_bansal_1ea593e019/day-2100-wait-dont-we-already-have-this-function-publishing-your-first-npm-package-5dce</guid>
      <description>&lt;p&gt;Another project, another déjà vu moment.&lt;/p&gt;

&lt;p&gt;I was about to copy-paste a helper function again when it hit me: “Didn’t I just use this in another repo?” Instead of repeating history, I finally did what I should’ve done long ago—packaged it up and published it to NPM.&lt;/p&gt;

&lt;p&gt;Publishing an NPM package is easier than you think, and once you do it, you’ll wonder why you didn’t sooner. No more hunting through old repos, just clean, reusable code.&lt;/p&gt;

&lt;p&gt;🛠️ Ready to publish yours? Start here: &lt;a href="https://link.medium.com/h2mfQqqn0Qb" rel="noopener noreferrer"&gt;https://link.medium.com/h2mfQqqn0Qb&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="https://medium.com/@utkarshbansal01/wait-dont-we-already-have-this-function-publishing-your-first-npm-package-37ddb6c02f39" class="ltag__link__link" rel="noopener noreferrer"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fda%3Atrue%2Fresize%3Afill%3A88%3A88%2F0%2AbpnV4_er4ixydXf2" alt="Utkarsh Bansal"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://medium.com/@utkarshbansal01/wait-dont-we-already-have-this-function-publishing-your-first-npm-package-37ddb6c02f39" class="ltag__link__link" rel="noopener noreferrer"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;“Wait, don’t we already have this function” — Publishing Your First NPM Package | by Utkarsh Bansal | Feb, 2025 | Medium&lt;/h2&gt;
      &lt;h3&gt;Utkarsh Bansal ・ &lt;time&gt;Feb 15, 2025&lt;/time&gt; ・ 
      &lt;div class="ltag__link__servicename"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fmedium-f709f79cf29704f9f4c2a83f950b2964e95007a3e311b77f686915c71574fef2.svg" alt="Medium Logo"&gt;
        Medium
      &lt;/div&gt;
    &lt;/h3&gt;
&lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Day 1/100: Exploring Frontend Storage 🖥️</title>
      <dc:creator>Utkarsh Bansal</dc:creator>
      <pubDate>Sat, 15 Feb 2025 17:17:05 +0000</pubDate>
      <link>https://dev.to/utkarsh_bansal_1ea593e019/day-1100-exploring-frontend-storage-2im8</link>
      <guid>https://dev.to/utkarsh_bansal_1ea593e019/day-1100-exploring-frontend-storage-2im8</guid>
      <description>&lt;p&gt;Modern frontend development isn't just about UI—it’s about efficient data management. From cookies to IndexedDB, each storage option plays a crucial role in performance, security, and user experience.&lt;/p&gt;

&lt;p&gt;​This is my first article where I break down key data management, performance optimization, and security best practices — while addressing key security issues and sharing industry insights… ​ &lt;/p&gt;

&lt;p&gt;​📖 Read: &lt;a href="https://lnkd.in/g-A6H87H" rel="noopener noreferrer"&gt;https://lnkd.in/g-A6H87H&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="https://medium.com/@utkarshbansal01/harnessing-frontend-storage-a-comprehensive-guide-to-browser-based-data-management-eaa7cf29d69f" class="ltag__link__link" rel="noopener noreferrer"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fda%3Atrue%2Fresize%3Afill%3A88%3A88%2F0%2AbpnV4_er4ixydXf2" alt="Utkarsh Bansal"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://medium.com/@utkarshbansal01/harnessing-frontend-storage-a-comprehensive-guide-to-browser-based-data-management-eaa7cf29d69f" class="ltag__link__link" rel="noopener noreferrer"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Harnessing Frontend Storage: A Comprehensive Guide to Browser-Based Data Management | by Utkarsh Bansal | Feb, 2025 | Medium&lt;/h2&gt;
      &lt;h3&gt;Utkarsh Bansal ・ &lt;time&gt;Feb 14, 2025&lt;/time&gt; ・ 
      &lt;div class="ltag__link__servicename"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fmedium-f709f79cf29704f9f4c2a83f950b2964e95007a3e311b77f686915c71574fef2.svg" alt="Medium Logo"&gt;
        Medium
      &lt;/div&gt;
    &lt;/h3&gt;
&lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
