<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dutta-roy-samrat.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dutta-roy-samrat.github.io/" rel="alternate" type="text/html" /><updated>2026-08-26T02:09:54+00:00</updated><id>https://dutta-roy-samrat.github.io/feed.xml</id><title type="html">Samrat Dutta Roy | Tech Blog</title><subtitle>Technical deep-dives on frontend architecture, browser engines, JavaScript internals, and web performance by a Senior Frontend Engineer.</subtitle><author><name>Samrat Dutta Roy</name></author><entry><title type="html">Why Your Next.js Images Are Slow: The Props You’re Probably Missing</title><link href="https://dutta-roy-samrat.github.io/posts/why-your-nextjs-images-are-slow/" rel="alternate" type="text/html" title="Why Your Next.js Images Are Slow: The Props You’re Probably Missing" /><published>2026-05-25T00:00:00+00:00</published><updated>2026-05-25T00:00:00+00:00</updated><id>https://dutta-roy-samrat.github.io/posts/why-your-nextjs-images-are-slow</id><content type="html" xml:base="https://dutta-roy-samrat.github.io/posts/why-your-nextjs-images-are-slow/"><![CDATA[<blockquote>
  <p><strong>Direct answer:</strong> The <code class="language-plaintext highlighter-rouge">next/image</code> component requires either <code class="language-plaintext highlighter-rouge">width</code> and <code class="language-plaintext highlighter-rouge">height</code> props or a <code class="language-plaintext highlighter-rouge">fill</code> prop to function correctly. When using <code class="language-plaintext highlighter-rouge">fill</code>, omitting the <code class="language-plaintext highlighter-rouge">sizes</code> prop causes the browser to default to <code class="language-plaintext highlighter-rouge">100vw</code> — treating the image as full-viewport-width and downloading the largest available version. On high-resolution desktop screens this means unnecessarily large image downloads. Providing accurate <code class="language-plaintext highlighter-rouge">sizes</code> values tells the browser the actual rendered size of the image, allowing it to download a much smaller, appropriately sized version.</p>
</blockquote>

<p>We were facing slow image loading across parts of our application. My first instinct was CDN caching — we weren’t caching images through a CDN the way we were with other static assets, and due to cost constraints that wasn’t going to change immediately.</p>

<p>But it felt too slow to be explained by CDN caching alone. A missing cache header adds latency, but what I was seeing felt heavier than that. So I dug deeper.</p>

<p>What I found was that our <code class="language-plaintext highlighter-rouge">next/image</code> components had two distinct problems: some were missing <code class="language-plaintext highlighter-rouge">width</code> and <code class="language-plaintext highlighter-rouge">height</code> props entirely, and the ones using <code class="language-plaintext highlighter-rouge">fill</code> had no <code class="language-plaintext highlighter-rouge">sizes</code> prop on any of them. Once I understood what those props actually do under the hood, the slowness made complete sense.</p>

<hr />

<h2 id="what-nextimage-actually-does">What <code class="language-plaintext highlighter-rouge">next/image</code> Actually Does</h2>

<p>Before getting into the props, it’s worth understanding what the <code class="language-plaintext highlighter-rouge">next/image</code> component gives you over a plain <code class="language-plaintext highlighter-rouge">&lt;img&gt;</code> tag — because this context makes the props make more sense.</p>

<p><code class="language-plaintext highlighter-rouge">next/image</code> automatically:</p>

<ul>
  <li>Converts images to modern formats like <strong>WebP</strong> (significantly smaller than JPEG or PNG)</li>
  <li>Generates multiple resized versions of the image at different widths</li>
  <li>Lazy loads images by default — only downloading when they approach the viewport</li>
  <li>Prevents <strong>Cumulative Layout Shift (CLS)</strong> by reserving space before the image loads</li>
</ul>

<p>The key phrase is “generates multiple resized versions.” Next.js builds a <code class="language-plaintext highlighter-rouge">srcset</code> — a list of the same image at different widths — and the browser picks which one to download based on the device’s screen width and pixel density.</p>

<p>The props you pass directly control how intelligently that selection happens.</p>

<hr />

<h2 id="the-first-problem-missing-width-and-height">The First Problem: Missing <code class="language-plaintext highlighter-rouge">width</code> and <code class="language-plaintext highlighter-rouge">height</code></h2>

<p>Some of our images had neither <code class="language-plaintext highlighter-rouge">width</code> nor <code class="language-plaintext highlighter-rouge">height</code> nor <code class="language-plaintext highlighter-rouge">fill</code>. This is an error — <code class="language-plaintext highlighter-rouge">next/image</code> requires one or the other.</p>

<p><strong>What <code class="language-plaintext highlighter-rouge">width</code> and <code class="language-plaintext highlighter-rouge">height</code> do:</strong></p>

<p>These props define the intrinsic dimensions of the image in pixels. They serve two purposes:</p>

<ol>
  <li><strong>They prevent layout shift.</strong> The browser knows how much space to reserve before the image loads, so nothing jumps around. This directly improves your CLS score.</li>
  <li><strong>They anchor the generated <code class="language-plaintext highlighter-rouge">srcset</code>.</strong> Next.js uses the <code class="language-plaintext highlighter-rouge">width</code> value as the largest size to generate, and creates smaller variants from there.</li>
</ol>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Correct — fixed dimensions</span>
<span class="p">&lt;</span><span class="nc">Image</span> <span class="na">src</span><span class="p">=</span><span class="s">"/hero.jpg"</span> <span class="na">alt</span><span class="p">=</span><span class="s">"Hero image"</span> <span class="na">width</span><span class="p">=</span><span class="si">{</span><span class="mi">800</span><span class="si">}</span> <span class="na">height</span><span class="p">=</span><span class="si">{</span><span class="mi">450</span><span class="si">}</span> <span class="p">/&gt;</span>
</code></pre></div></div>

<p>Without these, Next.js doesn’t know the intended size of the image, can’t generate an appropriate <code class="language-plaintext highlighter-rouge">srcset</code>, and can’t reserve layout space. You get a console error and undefined behaviour.</p>

<hr />

<h2 id="the-second-problem-fill-without-sizes">The Second Problem: <code class="language-plaintext highlighter-rouge">fill</code> Without <code class="language-plaintext highlighter-rouge">sizes</code></h2>

<p>The rest of our images were using the <code class="language-plaintext highlighter-rouge">fill</code> prop — which is the right choice when an image needs to fill its parent container rather than have fixed pixel dimensions. Common use cases are hero sections, card thumbnails, and background images.</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// fill — the image stretches to fill its parent</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="p">=&gt;</span>
  <span class="p">&lt;</span><span class="nc">Image</span> <span class="na">src</span><span class="p">=</span><span class="s">"/banner.jpg"</span> <span class="na">alt</span><span class="p">=</span><span class="s">"Banner"</span> <span class="na">fill</span> <span class="na">style</span><span class="p">=</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fill</code> works correctly. But without a <code class="language-plaintext highlighter-rouge">sizes</code> prop alongside it, something problematic happens.</p>

<p><strong>What <code class="language-plaintext highlighter-rouge">sizes</code> does and why it matters:</strong></p>

<p>When Next.js generates a <code class="language-plaintext highlighter-rouge">srcset</code> for an image, it creates multiple versions at different widths. The browser then has to choose which one to download. To make that choice, it needs to know how wide the image will actually be rendered on screen.</p>

<p>Here’s the problem: <strong>the browser makes this decision before the page’s CSS has loaded and been applied.</strong> It doesn’t yet know that your image is inside a two-column grid, or that it’s constrained to a 400px sidebar. All it knows is the viewport width.</p>

<p>So without a <code class="language-plaintext highlighter-rouge">sizes</code> hint from you, it falls back to the safest assumption: the image will be as wide as the viewport — <code class="language-plaintext highlighter-rouge">100vw</code>.</p>

<p>On a 1440px desktop screen with a 2x Retina display, <code class="language-plaintext highlighter-rouge">100vw</code> means the browser targets a 2880px wide image. Even if the image is only rendered at 400px. The result is downloading an image up to 7x larger than necessary.</p>

<p>This is confirmed in Next.js’s own documentation — <cite index="4-1">if you don’t specify a <code class="language-plaintext highlighter-rouge">sizes</code> value on an image with the <code class="language-plaintext highlighter-rouge">fill</code> property, a default value of <code class="language-plaintext highlighter-rouge">100vw</code> is used, and the browser selects an image that is the same size or larger than the viewport.</cite></p>

<p><strong>How <code class="language-plaintext highlighter-rouge">sizes</code> fixes this:</strong></p>

<p><code class="language-plaintext highlighter-rouge">sizes</code> is a hint to the browser describing how wide the image will actually appear at different viewport widths. It uses the same media query syntax as CSS.</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Image that's full-width on mobile, half-width on desktop</span>
<span class="p">&lt;</span><span class="nc">Image</span>
  <span class="na">src</span><span class="p">=</span><span class="s">"/banner.jpg"</span>
  <span class="na">alt</span><span class="p">=</span><span class="s">"Banner"</span>
  <span class="na">fill</span>
  <span class="na">sizes</span><span class="p">=</span><span class="s">"(max-width: 768px) 100vw, 50vw"</span>
  <span class="na">style</span><span class="p">=</span>
<span class="p">/&gt;</span>
</code></pre></div></div>

<p>Now the browser knows: on screens narrower than 768px, the image is full width — download accordingly. On wider screens, the image is half the viewport — download a smaller version.</p>

<p>For a 1440px desktop screen with <code class="language-plaintext highlighter-rouge">sizes="50vw"</code>, the browser targets a 720px image (or 1440px on Retina) instead of 2880px. That’s a dramatically smaller file.</p>

<hr />

<h2 id="desktop-vs-mobile-where-the-problem-hits-hardest">Desktop vs Mobile: Where the Problem Hits Hardest</h2>

<p>The missing <code class="language-plaintext highlighter-rouge">sizes</code> problem is worse on desktop than mobile — but for a reason that’s slightly counterintuitive.</p>

<p>On mobile, viewport widths are smaller (typically 375–430px). Even with the <code class="language-plaintext highlighter-rouge">100vw</code> default, the browser is selecting images for a ~400px wide screen. The mismatch between “actual render size” and “assumed viewport size” is smaller, so the wasted bytes are less severe.</p>

<p>On desktop, viewports are 1280–1920px, and many devices are high-DPI (2x or 3x pixel ratio). The browser targets <code class="language-plaintext highlighter-rouge">viewport width × pixel ratio</code> — so a 1440px screen at 2x means targeting a 2880px image. If your image is actually rendered at 300px (say, a card thumbnail in a grid), you’re downloading nearly 10x more data than needed.</p>

<p>This is why the slowness was more noticeable in grid layouts and multi-column designs than on single-column mobile views.</p>

<hr />

<h2 id="the-sizes-prop-in-practice">The <code class="language-plaintext highlighter-rouge">sizes</code> Prop in Practice</h2>

<p>Here are the patterns we used to fix our images:</p>

<p><strong>Full-width hero image:</strong></p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">Image</span>
  <span class="na">src</span><span class="p">=</span><span class="s">"/hero.jpg"</span>
  <span class="na">alt</span><span class="p">=</span><span class="s">"Hero"</span>
  <span class="na">fill</span>
  <span class="na">sizes</span><span class="p">=</span><span class="s">"100vw"</span>
  <span class="na">style</span><span class="p">=</span>
<span class="p">/&gt;</span>
</code></pre></div></div>

<p><strong>Image in a two-column grid:</strong></p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">Image</span>
  <span class="na">src</span><span class="p">=</span><span class="s">"/card.jpg"</span>
  <span class="na">alt</span><span class="p">=</span><span class="s">"Card"</span>
  <span class="na">fill</span>
  <span class="na">sizes</span><span class="p">=</span><span class="s">"(max-width: 768px) 100vw, 50vw"</span>
  <span class="na">style</span><span class="p">=</span>
<span class="p">/&gt;</span>
</code></pre></div></div>

<p><strong>Image in a three-column grid:</strong></p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">Image</span>
  <span class="na">src</span><span class="p">=</span><span class="s">"/thumbnail.jpg"</span>
  <span class="na">alt</span><span class="p">=</span><span class="s">"Thumbnail"</span>
  <span class="na">fill</span>
  <span class="na">sizes</span><span class="p">=</span><span class="s">"(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"</span>
  <span class="na">style</span><span class="p">=</span>
<span class="p">/&gt;</span>
</code></pre></div></div>

<p><strong>Image in a fixed-width sidebar:</strong></p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="nc">Image</span>
  <span class="na">src</span><span class="p">=</span><span class="s">"/profile.jpg"</span>
  <span class="na">alt</span><span class="p">=</span><span class="s">"Profile"</span>
  <span class="na">fill</span>
  <span class="na">sizes</span><span class="p">=</span><span class="s">"240px"</span>
  <span class="na">style</span><span class="p">=</span>
<span class="p">/&gt;</span>
</code></pre></div></div>

<p>The values don’t need to be pixel-perfect. A reasonable approximation is enough — the browser uses them to select from a pre-generated <code class="language-plaintext highlighter-rouge">srcset</code>, and picking the next size up is always acceptable. What you’re avoiding is the browser treating a 240px sidebar image as a full-viewport image.</p>

<hr />

<h2 id="a-quick-reference-for-all-the-props">A Quick Reference for All the Props</h2>

<table>
  <thead>
    <tr>
      <th>Prop</th>
      <th>Required</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">src</code></td>
      <td>✅ Always</td>
      <td>Image URL or imported file</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">alt</code></td>
      <td>✅ Always</td>
      <td>Accessibility and SEO</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">width</code> + <code class="language-plaintext highlighter-rouge">height</code></td>
      <td>✅ Unless using <code class="language-plaintext highlighter-rouge">fill</code></td>
      <td>Fixed dimensions in px — prevents layout shift</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fill</code></td>
      <td>✅ Unless using <code class="language-plaintext highlighter-rouge">width</code>/<code class="language-plaintext highlighter-rouge">height</code></td>
      <td>Stretches image to fill parent container</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sizes</code></td>
      <td>⚠️ Always recommended</td>
      <td>Tells browser actual render width — required with <code class="language-plaintext highlighter-rouge">fill</code>, strongly recommended with <code class="language-plaintext highlighter-rouge">width</code>/<code class="language-plaintext highlighter-rouge">height</code> for responsive images</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">priority</code></td>
      <td>Optional</td>
      <td>Disables lazy loading — use for above-the-fold images (LCP)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">quality</code></td>
      <td>Optional</td>
      <td>JPEG/WebP compression quality, 1–100 (default 75)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">placeholder</code></td>
      <td>Optional</td>
      <td><code class="language-plaintext highlighter-rouge">"blur"</code> shows a blurred placeholder while loading</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="what-actually-fixed-the-slowness">What Actually Fixed the Slowness</h2>

<p>After adding proper <code class="language-plaintext highlighter-rouge">width</code> and <code class="language-plaintext highlighter-rouge">height</code> to fixed images and <code class="language-plaintext highlighter-rouge">sizes</code> to all <code class="language-plaintext highlighter-rouge">fill</code> images, the improvement was noticeable — especially in grid layouts on desktop.</p>

<p>The CDN caching issue was real and still on the list to address. But it wasn’t the primary cause. The browser was downloading images 5–10x larger than necessary on every page load, on every device, on every visit — cached or not. Fixing the props reduced the image payload significantly before any caching optimisation was even applied.</p>

<p>The lesson: <code class="language-plaintext highlighter-rouge">next/image</code> gives you a lot for free, but it can only optimise based on what you tell it. Without <code class="language-plaintext highlighter-rouge">width</code>/<code class="language-plaintext highlighter-rouge">height</code>, it doesn’t know the image’s intended size. Without <code class="language-plaintext highlighter-rouge">sizes</code>, it assumes full-viewport. Both omissions lead to the same outcome — oversized images served to every user.</p>

<hr />

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<p><strong>Why are my Next.js images loading slowly despite using <code class="language-plaintext highlighter-rouge">next/image</code>?</strong>
The most common causes are missing <code class="language-plaintext highlighter-rouge">width</code> and <code class="language-plaintext highlighter-rouge">height</code> props on fixed images, or a missing <code class="language-plaintext highlighter-rouge">sizes</code> prop on images using <code class="language-plaintext highlighter-rouge">fill</code>. Without <code class="language-plaintext highlighter-rouge">sizes</code>, the browser defaults to assuming the image is full-viewport-width and downloads the largest available version — even if the image is only rendered at a fraction of that size.</p>

<p><strong>What does the <code class="language-plaintext highlighter-rouge">sizes</code> prop do in <code class="language-plaintext highlighter-rouge">next/image</code>?</strong>
<code class="language-plaintext highlighter-rouge">sizes</code> tells the browser how wide the image will actually be rendered at different viewport widths, using media query syntax. The browser uses this to pick the most appropriate version from Next.js’s auto-generated <code class="language-plaintext highlighter-rouge">srcset</code>. Without <code class="language-plaintext highlighter-rouge">sizes</code> on a <code class="language-plaintext highlighter-rouge">fill</code> image, the browser defaults to <code class="language-plaintext highlighter-rouge">100vw</code> and downloads the largest available version. With <code class="language-plaintext highlighter-rouge">width</code> and <code class="language-plaintext highlighter-rouge">height</code>, omitting <code class="language-plaintext highlighter-rouge">sizes</code> generates only a fixed 1x and 2x <code class="language-plaintext highlighter-rouge">srcset</code> — which works, but providing <code class="language-plaintext highlighter-rouge">sizes</code> unlocks a fuller responsive <code class="language-plaintext highlighter-rouge">srcset</code> for more precise optimisation.</p>

<p><strong>What is the difference between <code class="language-plaintext highlighter-rouge">fill</code> and <code class="language-plaintext highlighter-rouge">width</code>/<code class="language-plaintext highlighter-rouge">height</code> in <code class="language-plaintext highlighter-rouge">next/image</code>?</strong>
<code class="language-plaintext highlighter-rouge">width</code> and <code class="language-plaintext highlighter-rouge">height</code> define fixed pixel dimensions for the image. <code class="language-plaintext highlighter-rouge">fill</code> makes the image stretch to fill its parent container — useful for responsive layouts where the image should cover a variable-sized area. When using <code class="language-plaintext highlighter-rouge">fill</code>, the parent must have <code class="language-plaintext highlighter-rouge">position: relative</code> set, and you should always add a <code class="language-plaintext highlighter-rouge">sizes</code> prop to prevent oversized image downloads.</p>

<p><strong>Does missing <code class="language-plaintext highlighter-rouge">sizes</code> affect mobile or desktop more?</strong>
Desktop is affected more severely. Mobile viewports are narrower, so the gap between “assumed full-viewport size” and “actual render size” is smaller. On desktop, especially high-DPI screens where the browser targets <code class="language-plaintext highlighter-rouge">viewport width × pixel ratio</code>, the mismatch can result in images 5–10x larger than what’s actually rendered.</p>

<p><strong>When should you use the <code class="language-plaintext highlighter-rouge">priority</code> prop on <code class="language-plaintext highlighter-rouge">next/image</code>?</strong>
Use <code class="language-plaintext highlighter-rouge">priority</code> on any image that is the Largest Contentful Paint (LCP) element — typically the main hero image or the first visible image above the fold. <code class="language-plaintext highlighter-rouge">priority</code> disables lazy loading and preloads the image, improving LCP scores. Avoid using it on below-the-fold images as it defeats the purpose of lazy loading.</p>

<hr />

<p><em>Co-written with AI.</em></p>]]></content><author><name>Samrat Dutta Roy</name></author><category term="next.js" /><category term="next/image" /><category term="image optimization" /><category term="sizes prop" /><category term="fill prop" /><category term="width height props" /><category term="web performance" /><category term="Core Web Vitals" /><category term="LCP" /><category term="frontend" /><category term="slow images" /><category term="srcset" /><summary type="html"><![CDATA[Our images were loading too slowly. CDN caching wasn't enabled, but the slowness felt like something more. Diving deeper revealed that missing width, height, and sizes props on next/image components were forcing the browser to download the largest available image every time.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dutta-roy-samrat.github.io/assets/images/nextjs-image-optimization.png" /><media:content medium="image" url="https://dutta-roy-samrat.github.io/assets/images/nextjs-image-optimization.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why Did My Already-Loaded Component Show a Skeleton Again? React Suspense Bubbling Explained</title><link href="https://dutta-roy-samrat.github.io/posts/react-suspense-bubbling-explained/" rel="alternate" type="text/html" title="Why Did My Already-Loaded Component Show a Skeleton Again? React Suspense Bubbling Explained" /><published>2026-05-24T00:00:00+00:00</published><updated>2026-05-24T00:00:00+00:00</updated><id>https://dutta-roy-samrat.github.io/posts/react-suspense-bubbling-explained</id><content type="html" xml:base="https://dutta-roy-samrat.github.io/posts/react-suspense-bubbling-explained/"><![CDATA[<blockquote>
  <p><strong>Direct answer:</strong> In React, when a dynamically imported component suspends, it throws a Promise up the component tree until the nearest Suspense boundary catches it. If a nested modal has no local Suspense boundary, its suspension can bubble up and trigger a loading skeleton for an entirely unrelated parent component — even one that was already fully loaded. The fix is ensuring every dynamic import has its own local Suspense boundary via a <code class="language-plaintext highlighter-rouge">loading</code> option.</p>
</blockquote>

<p>I was working on a feature that involved opening a modal deep inside a page. The modal was dynamically imported — standard practice for keeping the initial bundle small. Everything looked fine until I noticed something that made no sense:</p>

<p>Opening the modal was making a completely separate part of the page — one that had already loaded — flash back to its skeleton loader.</p>

<p>My mental model at the time was simple: <strong>once a component loads, it stays loaded.</strong> You see the skeleton once, the component appears, and that’s it. The skeleton is gone forever.</p>

<p>That assumption was wrong. And understanding <em>why</em> it was wrong taught me more about how React works under the hood than almost anything else I’ve encountered.</p>

<hr />

<h2 id="what-dynamic-imports-actually-do-under-the-hood">What Dynamic Imports Actually Do Under the Hood</h2>

<p>To understand the bug, you need to understand what <code class="language-plaintext highlighter-rouge">dynamic()</code> in Next.js — or <code class="language-plaintext highlighter-rouge">React.lazy()</code> in plain React — actually does at runtime.</p>

<p>When you write:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">PreApproveModal</span> <span class="o">=</span> <span class="nx">dynamic</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./PreApproveModal</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span>
  <span class="na">loading</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kc">null</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>

<p>React doesn’t just “load the component lazily.” It does something very specific: <strong>it throws a Promise.</strong></p>

<p>When your code tries to render <code class="language-plaintext highlighter-rouge">&lt;PreApproveModal /&gt;</code> and the JavaScript chunk for that component hasn’t been downloaded yet, React throws a Promise as an exception up the component tree. This is not a metaphor — it literally uses JavaScript’s <code class="language-plaintext highlighter-rouge">throw</code> mechanism.</p>

<p>React then walks up the component tree looking for the nearest <code class="language-plaintext highlighter-rouge">&lt;Suspense&gt;</code> boundary. When it finds one, it renders that boundary’s <code class="language-plaintext highlighter-rouge">fallback</code> prop while it waits for the Promise to resolve — meaning while the chunk downloads. Once the chunk is ready, React re-renders the component normally.</p>

<p>The <code class="language-plaintext highlighter-rouge">{ loading: () =&gt; null }</code> option in <code class="language-plaintext highlighter-rouge">dynamic()</code> is shorthand for Next.js automatically wrapping that component in a <code class="language-plaintext highlighter-rouge">&lt;Suspense fallback={loading()}&gt;</code> boundary. It’s a local catch for the thrown Promise — right at the source.</p>

<hr />

<h2 id="the-bug-what-was-actually-happening">The Bug: What Was Actually Happening</h2>

<p>Here’s a simplified version of the component tree I was working with:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// RecipientsContent — loaded dynamically with a skeleton</span>
<span class="kd">const</span> <span class="nx">RecipientsContent</span> <span class="o">=</span> <span class="nx">dynamic</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./RecipientsContent</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span>
  <span class="na">loading</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">&lt;</span><span class="nc">StepContentSkeleton</span> <span class="p">/&gt;,</span>
<span class="p">});</span>

<span class="c1">// PreApproveModal — loaded dynamically, loading option commented out</span>
<span class="kd">const</span> <span class="nx">PreApproveModal</span> <span class="o">=</span> <span class="nx">dynamic</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./PreApproveModal</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span>
  <span class="c1">// loading: () =&gt; null   &lt;-- this was commented out</span>
<span class="p">});</span>

<span class="kd">function</span> <span class="nx">Page</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="p">&lt;</span><span class="nc">RecipientsContent</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nc">PreApproveModal</span> <span class="na">isOpen</span><span class="p">=</span><span class="si">{</span><span class="nx">isOpen</span><span class="si">}</span> <span class="p">/&gt;</span>
    <span class="p">&lt;/</span><span class="nc">RecipientsContent</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The chain of events when I opened the modal:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">&lt;PreApproveModal /&gt;</code> tries to render for the first time</li>
  <li>Its JavaScript chunk hasn’t downloaded yet — React throws a Promise</li>
  <li>React walks up the tree looking for the nearest <code class="language-plaintext highlighter-rouge">&lt;Suspense&gt;</code> boundary to catch it</li>
  <li>There’s no local boundary around <code class="language-plaintext highlighter-rouge">PreApproveModal</code> — the <code class="language-plaintext highlighter-rouge">loading</code> option was commented out</li>
  <li>The Promise keeps bubbling up the tree</li>
  <li>It hits the <code class="language-plaintext highlighter-rouge">&lt;Suspense&gt;</code> boundary created by <code class="language-plaintext highlighter-rouge">RecipientsContent</code>’s <code class="language-plaintext highlighter-rouge">{ loading: () =&gt; &lt;StepContentSkeleton /&gt; }</code></li>
  <li>React hides the entire <code class="language-plaintext highlighter-rouge">RecipientsContent</code> tree and shows <code class="language-plaintext highlighter-rouge">&lt;StepContentSkeleton /&gt;</code> while the modal chunk downloads</li>
</ol>

<p>The result: opening a small modal caused the entire parent section — already fully rendered, already fully loaded — to disappear and show its skeleton again.</p>

<hr />

<h2 id="why-my-mental-model-was-wrong">Why My Mental Model Was Wrong</h2>

<p>My assumption was: <em>once a component loads, it stays loaded. The skeleton only appears once.</em></p>

<p>This is true for the component that’s already loaded. <code class="language-plaintext highlighter-rouge">RecipientsContent</code> itself was not re-downloading. Its chunk was already in the browser.</p>

<p>But Suspense doesn’t work at the chunk level. It works at the <strong>render level</strong>. When a Promise is thrown anywhere inside a Suspense boundary’s subtree — from any child, at any depth — React unmounts the entire subtree and shows the fallback. It doesn’t matter that most of that subtree was already loaded and rendered. The entire boundary resets.</p>

<p>The key insight: <strong>Suspense boundaries don’t protect against re-showing their fallback. They catch any suspension from any descendant, at any time, regardless of whether those descendants were previously loaded.</strong></p>

<p>Once I understood that, the bug made complete sense.</p>

<hr />

<h2 id="why-it-stopped-when-i-changed-things">Why It Stopped When I Changed Things</h2>

<p><strong>Uncommenting <code class="language-plaintext highlighter-rouge">{ loading: () =&gt; null }</code> on the modal:</strong></p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">PreApproveModal</span> <span class="o">=</span> <span class="nx">dynamic</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./PreApproveModal</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span>
  <span class="na">loading</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kc">null</span><span class="p">,</span> <span class="c1">// ← this creates a local Suspense boundary</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Now when <code class="language-plaintext highlighter-rouge">PreApproveModal</code> throws its Promise, there’s a local <code class="language-plaintext highlighter-rouge">&lt;Suspense fallback={null}&gt;</code> boundary right around it to catch it. The Promise never bubbles up. <code class="language-plaintext highlighter-rouge">RecipientsContent</code> never sees it. The skeleton never appears. The modal renders <code class="language-plaintext highlighter-rouge">null</code> briefly while its chunk downloads, then appears — and the rest of the page stays perfectly intact.</p>

<p><strong>Removing <code class="language-plaintext highlighter-rouge">{ loading: () =&gt; &lt;StepContentSkeleton /&gt; }</code> from <code class="language-plaintext highlighter-rouge">RecipientsContent</code>:</strong></p>

<p>Without a boundary on <code class="language-plaintext highlighter-rouge">RecipientsContent</code>, the bubbling Promise skips past it entirely and travels up to the next boundary — the page level or wherever the next <code class="language-plaintext highlighter-rouge">&lt;Suspense&gt;</code> lives. You stop seeing <code class="language-plaintext highlighter-rouge">StepContentSkeleton</code> because that specific boundary no longer exists to catch it. But the suspension is still happening — it’s just caught higher up.</p>

<hr />

<h2 id="the-fix-and-the-principle-behind-it">The Fix and the Principle Behind It</h2>

<p>The fix is straightforward:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Always give deeply nested dynamic imports their own boundary</span>
<span class="kd">const</span> <span class="nx">PreApproveModal</span> <span class="o">=</span> <span class="nx">dynamic</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./PreApproveModal</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span>
  <span class="na">loading</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kc">null</span><span class="p">,</span> <span class="c1">// renders nothing while loading — modal isn't visible yet anyway</span>
<span class="p">});</span>

<span class="kd">const</span> <span class="nx">HeavyTooltip</span> <span class="o">=</span> <span class="nx">dynamic</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./HeavyTooltip</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span>
  <span class="na">loading</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kc">null</span><span class="p">,</span>
<span class="p">});</span>

<span class="kd">const</span> <span class="nx">SidePanel</span> <span class="o">=</span> <span class="nx">dynamic</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./SidePanel</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span>
  <span class="na">loading</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="o">&lt;</span><span class="nx">SidePanelSkeleton</span> <span class="o">/&gt;</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>

<p>The principle: <strong>every dynamically imported component should own its Suspense boundary.</strong> Not just the top-level ones. Especially the small, deeply nested ones — modals, tooltips, drawers — because those are the ones most likely to be opened after the page has already rendered, and their suspension has the furthest to bubble before it hits something.</p>

<p>For components where no visible loading state makes sense — a modal that isn’t visible until it opens — <code class="language-plaintext highlighter-rouge">loading: () =&gt; null</code> is the right choice. It creates the boundary, catches the suspension locally, renders nothing while loading, and lets everything else on the page continue undisturbed.</p>

<hr />

<h2 id="visualising-the-suspense-tree">Visualising the Suspense Tree</h2>

<p>Here’s how the component tree looks before and after the fix:</p>

<p><strong>Before (broken):</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;Page&gt;
  &lt;Suspense fallback={&lt;StepContentSkeleton /&gt;}&gt;   ← RecipientsContent's boundary
    &lt;RecipientsContent&gt;                            ← already loaded ✓
      &lt;PreApproveModal /&gt;                          ← throws Promise 💥
        (no local boundary — Promise bubbles up)
    &lt;/RecipientsContent&gt;
  &lt;/Suspense&gt;                                      ← catches it here
                                                   → hides ALL of RecipientsContent
                                                   → shows StepContentSkeleton ❌
</code></pre></div></div>

<p><strong>After (fixed):</strong></p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;Page&gt;
  &lt;Suspense fallback={&lt;StepContentSkeleton /&gt;}&gt;   ← RecipientsContent's boundary
    &lt;RecipientsContent&gt;                            ← stays rendered ✓
      &lt;Suspense fallback={null}&gt;                   ← PreApproveModal's local boundary
        &lt;PreApproveModal /&gt;                        ← throws Promise 💥
      &lt;/Suspense&gt;                                  ← caught here locally ✓
                                                   → renders null briefly
                                                   → RecipientsContent untouched ✓
    &lt;/RecipientsContent&gt;
  &lt;/Suspense&gt;
</code></pre></div></div>

<hr />

<h2 id="what-this-taught-me">What This Taught Me</h2>

<p>The bug came from a gap between how I thought React lazy loading worked and how it actually works.</p>

<p>I thought of dynamic imports as a one-time cost — the component loads, the skeleton disappears, and from that point on the component behaves like any other. In terms of the chunk download, that’s true. But in terms of Suspense, the boundary doesn’t know or care whether the chunk was previously loaded. It only knows whether something inside its subtree is currently suspended.</p>

<p>The moment I understood that Suspense boundaries catch thrown Promises from any descendant regardless of load history, the entire behaviour became predictable. And once behaviour is predictable, bugs stop being mysterious.</p>

<p>Every dynamic import is a potential Promise throw. Give it a boundary close enough to catch it before it reaches something you don’t want interrupted.</p>

<hr />

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<p><strong>What is React Suspense bubbling?</strong>
Suspense bubbling is what happens when a component throws a Promise — the standard mechanism for React lazy loading — and there is no local Suspense boundary to catch it. The Promise travels up the component tree until it reaches the nearest ancestor Suspense boundary, which then unmounts its entire subtree and shows its fallback UI.</p>

<p><strong>Why does a skeleton appear again for a component that was already loaded?</strong>
A Suspense boundary catches any suspension from any descendant, at any time. Even if the parent component itself is fully loaded, a child component suspending inside its tree causes the entire Suspense subtree to unmount and show the fallback. The parent’s load state is irrelevant — what matters is whether any descendant is currently suspended.</p>

<p><strong>What does <code class="language-plaintext highlighter-rouge">{ loading: () =&gt; null }</code> do in Next.js dynamic imports?</strong>
It creates a local Suspense boundary around the dynamically imported component with <code class="language-plaintext highlighter-rouge">null</code> as the fallback. When the component suspends while its chunk downloads, the suspension is caught locally and renders nothing. This prevents the suspension from bubbling up to a parent Suspense boundary and disrupting already-rendered parts of the page.</p>

<p><strong>How does <code class="language-plaintext highlighter-rouge">React.lazy</code> work under the hood?</strong>
When a lazily imported component renders before its JavaScript chunk has downloaded, React throws a Promise as an exception. React catches this using the nearest Suspense boundary up the component tree and renders the boundary’s fallback UI. When the Promise resolves — meaning the chunk has downloaded — React re-renders the component normally.</p>

<p><strong>When should you use <code class="language-plaintext highlighter-rouge">loading: () =&gt; null</code> vs a real skeleton in dynamic imports?</strong>
Use <code class="language-plaintext highlighter-rouge">loading: () =&gt; null</code> for components that are not visible until triggered by user action — modals, drawers, tooltips, popovers. They don’t need a visible loading state because the user triggered them and the download is typically fast. Use a real skeleton for components that are visible on initial render — page sections, content areas — where the user needs a placeholder while the content loads. Also use a real skeleton for any component that is genuinely heavy to download, regardless of when it appears — if the chunk is large enough that the user will notice a delay, a skeleton gives them feedback that something is on its way.</p>

<hr />

<p><em>Co-written with AI.</em></p>]]></content><author><name>Samrat Dutta Roy</name></author><category term="react" /><category term="suspense" /><category term="dynamic import" /><category term="next.js" /><category term="suspense bubbling" /><category term="lazy loading" /><category term="skeleton loader" /><category term="react internals" /><category term="frontend debugging" /><category term="javascript" /><category term="react performance" /><summary type="html"><![CDATA[I assumed once a React component loads, it stays loaded and you never see its skeleton again. A bug involving a modal and a misplaced Suspense boundary proved me wrong — and taught me how dynamic imports actually work under the hood.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dutta-roy-samrat.github.io/assets/images/react-suspense-bubbling.png" /><media:content medium="image" url="https://dutta-roy-samrat.github.io/assets/images/react-suspense-bubbling.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Functions vs Classes in JavaScript: When to Use Which and Why It Actually Matters</title><link href="https://dutta-roy-samrat.github.io/posts/functions-vs-classes-in-javascript-when-to-use-which-and-why-it-actually-matters/" rel="alternate" type="text/html" title="Functions vs Classes in JavaScript: When to Use Which and Why It Actually Matters" /><published>2026-05-23T00:00:00+00:00</published><updated>2026-05-23T00:00:00+00:00</updated><id>https://dutta-roy-samrat.github.io/posts/functions-vs-classes-in-javascript-when-to-use-which-and-why-it-actually-matters</id><content type="html" xml:base="https://dutta-roy-samrat.github.io/posts/functions-vs-classes-in-javascript-when-to-use-which-and-why-it-actually-matters/"><![CDATA[<blockquote>
  <p><strong>Direct answer:</strong> Functions and classes are not competing tools — they solve different problems. Functions excel at stateless logic, composition, and modern UI patterns. Classes excel at encapsulating long-lived state, modelling domain entities, and memory-efficient instantiation at scale. The difference comes down to how JavaScript manages memory through the prototype chain, and what your specific use case actually demands.</p>
</blockquote>

<p>Look at modern software development and a pattern is hard to miss.</p>

<p><strong>React</strong> — the most widely used frontend framework — moved decisively away from class components toward <strong>functional components with Hooks</strong>. The community followed without much resistance.</p>

<p><strong>Django</strong> — one of the most battle-tested backend frameworks in existence — started with only function-based views. Class-based views came later, and to this day a vocal segment of experienced Django developers argue the tradeoff wasn’t worth it — that the inheritance chains made code harder to follow than the boilerplate they replaced.</p>

<p><strong>JavaScript itself</strong> has been trending toward functional patterns for years. Closures, higher-order functions, <code class="language-plaintext highlighter-rouge">map</code>, <code class="language-plaintext highlighter-rouge">filter</code>, <code class="language-plaintext highlighter-rouge">reduce</code> — these are the idioms that define modern JS.</p>

<p>So the reasonable question to ask is: <strong>if functions vs classes is a debate, and functions keep winning in practice — why do classes still exist at all?</strong></p>

<p>The answer isn’t that classes are legacy code waiting to be deprecated. It’s that functions and classes solve genuinely different problems — and understanding which problem each solves requires going a level deeper than syntax.</p>

<hr />

<h2 id="what-is-the-difference-between-a-function-and-a-class-in-javascript">What Is the Difference Between a Function and a Class in JavaScript?</h2>

<p>Ask most developers and you’ll get the standard answer: classes are for <strong>encapsulation</strong> and <strong>abstraction</strong> — bundling state and behaviour together, hiding internal details, modelling real-world entities.</p>

<p>Functions are more <strong>flexible</strong> and <strong>composable</strong> — great for pure logic, transformations, utility work.</p>

<p>That’s not wrong. But it’s incomplete. Because the obvious follow-up is: <em>can’t functions do encapsulation too?</em></p>

<p>Yes. They can. And that’s where things get interesting.</p>

<hr />

<h2 id="can-functions-replace-classes-closures-and-encapsulation-explained">Can Functions Replace Classes? Closures and Encapsulation Explained</h2>

<p>Before ES6 introduced the <code class="language-plaintext highlighter-rouge">class</code> keyword in 2015, JavaScript had no native class syntax. Developers who needed encapsulation and private state used <strong>closures</strong> — functions that trap variables inside their lexical scope and expose only what they choose to expose.</p>

<p>Here’s the same bank account implemented two ways:</p>

<p><strong>The Class approach:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">BankAccount</span> <span class="p">{</span>
  <span class="err">#</span><span class="nx">balance</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// # makes it a truly private field</span>

  <span class="nx">deposit</span><span class="p">(</span><span class="nx">amount</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">balance</span> <span class="o">+=</span> <span class="nx">amount</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nx">getBalance</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">balance</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>The Closure / Factory Function approach:</strong></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">createBankAccount</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">balance</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// Trapped in the closure — inaccessible from outside</span>

  <span class="k">return</span> <span class="p">{</span>
    <span class="nx">deposit</span><span class="p">(</span><span class="na">amount</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">balance</span> <span class="o">+=</span> <span class="nx">amount</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="nx">getBalance</span><span class="p">()</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">balance</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">};</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">account</span> <span class="o">=</span> <span class="nx">createBankAccount</span><span class="p">();</span>
<span class="nx">account</span><span class="p">.</span><span class="nx">deposit</span><span class="p">(</span><span class="mi">100</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">account</span><span class="p">.</span><span class="nx">getBalance</span><span class="p">());</span> <span class="c1">// 100</span>
<span class="c1">// balance is completely inaccessible from outside</span>
</code></pre></div></div>

<p>Both achieve the same result. Both hide internal state. Both expose only controlled methods.</p>

<p>So again — why do classes exist?</p>

<hr />

<h2 id="the-real-difference-memory-allocation-and-the-javascript-prototype-chain">The Real Difference: Memory Allocation and the JavaScript Prototype Chain</h2>

<p>This is where most explanations stop being vague and start being precise.</p>

<p>When you use the factory function approach, JavaScript creates a <strong>new set of function definitions in memory every single time</strong> you call <code class="language-plaintext highlighter-rouge">createBankAccount</code>. Each instance gets its own copy of <code class="language-plaintext highlighter-rouge">deposit</code> and <code class="language-plaintext highlighter-rouge">getBalance</code> — distinct function objects, each allocated separately in the heap.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">account1</span> <span class="o">=</span> <span class="nx">createBankAccount</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">account2</span> <span class="o">=</span> <span class="nx">createBankAccount</span><span class="p">();</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">account1</span><span class="p">.</span><span class="nx">deposit</span> <span class="o">===</span> <span class="nx">account2</span><span class="p">.</span><span class="nx">deposit</span><span class="p">);</span> <span class="c1">// false — different function objects</span>
</code></pre></div></div>

<p>Here’s what the memory heap actually looks like with 3 instances:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ Heap Memory ]

account1 ---&gt; { deposit: [Function Ref A], getBalance: [Function Ref B] } ---&gt; Closure (balance: 0)
account2 ---&gt; { deposit: [Function Ref C], getBalance: [Function Ref D] } ---&gt; Closure (balance: 0)
account3 ---&gt; { deposit: [Function Ref E], getBalance: [Function Ref F] } ---&gt; Closure (balance: 0)
</code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">deposit</code> contains 100 lines of logic, those 100 lines exist <strong>3 times</strong> in memory. With 10,000 instances, they exist 10,000 times.</p>

<p>Classes solve this with the <strong>JavaScript prototype chain</strong>. When you define a class, its methods are created exactly once — on the class’s prototype object. Every instance created with <code class="language-plaintext highlighter-rouge">new</code> simply holds a lightweight reference pointing back to those shared methods.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user1</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">BankAccount</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">user2</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">BankAccount</span><span class="p">();</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">user1</span><span class="p">.</span><span class="nx">deposit</span> <span class="o">===</span> <span class="nx">user2</span><span class="p">.</span><span class="nx">deposit</span><span class="p">);</span> <span class="c1">// true — same function object</span>
</code></pre></div></div>

<p>Memory heap with 3 instances using a class:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ Heap Memory ]

account1 ---&gt; { #balance: 0 } ---\
account2 ---&gt; { #balance: 0 } ----&gt; [ BankAccount.prototype ] ---&gt; { deposit: [Single Fn], getBalance: [Single Fn] }
account3 ---&gt; { #balance: 0 } ---/
</code></pre></div></div>

<p>No matter how many instances you create, the method logic exists exactly once. Every instance just holds a pointer to it.</p>

<p><strong>This is the real reason classes exist at scale.</strong> Not syntax preference. Not OOP philosophy. Memory efficiency through the prototype chain.</p>

<hr />

<h2 id="javascript-classes-are-just-syntactic-sugar--you-can-do-the-same-with-functions">JavaScript Classes Are Just Syntactic Sugar — You Can Do the Same with Functions</h2>

<p>This is the part that shows you understand JavaScript’s actual engine, not just its syntax.</p>

<p>The <code class="language-plaintext highlighter-rouge">class</code> keyword is syntactic sugar. Under the hood, JavaScript has always been prototype-based — there are no "real" classes like Java or C++. Before 2015, developers achieved the same memory efficiency manually using constructor functions and <code class="language-plaintext highlighter-rouge">.prototype</code>:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">BankAccount</span><span class="p">(</span><span class="nx">initialBalance</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">_balance</span> <span class="o">=</span> <span class="nx">initialBalance</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// Methods on the prototype — shared across all instances in memory</span>
<span class="nx">BankAccount</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">deposit</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">amount</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">_balance</span> <span class="o">+=</span> <span class="nx">amount</span><span class="p">;</span>
<span class="p">};</span>

<span class="nx">BankAccount</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">getBalance</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">_balance</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">a</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">BankAccount</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">b</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">BankAccount</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">deposit</span> <span class="o">===</span> <span class="nx">b</span><span class="p">.</span><span class="nx">deposit</span><span class="p">);</span> <span class="c1">// true — same reference</span>
</code></pre></div></div>

<p>Same memory layout as a class. Methods defined once, shared via prototype.</p>

<p>But notice the catch: to use prototype methods, internal state has to live on <code class="language-plaintext highlighter-rouge">this</code> — meaning <code class="language-plaintext highlighter-rouge">this._balance</code> is technically accessible from outside. You lose the strict privacy that closures give you.</p>

<p>This is the core trade-off between functions and classes in JavaScript:</p>

<table>
  <thead>
    <tr>
      <th>Approach</th>
      <th>Data Privacy</th>
      <th>Memory Efficiency</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Closure / Factory Function</td>
      <td>✅ Absolute — closure scope</td>
      <td>❌ New function copies per instance</td>
    </tr>
    <tr>
      <td>Constructor + Prototype</td>
      <td>❌ State exposed via <code class="language-plaintext highlighter-rouge">this</code></td>
      <td>✅ Methods shared via prototype</td>
    </tr>
    <tr>
      <td>ES6 Class with <code class="language-plaintext highlighter-rouge">#</code> fields</td>
      <td>✅ True private fields</td>
      <td>✅ Methods on prototype</td>
    </tr>
  </tbody>
</table>

<p>Modern ES6 classes with private fields (<code class="language-plaintext highlighter-rouge">#property</code>) are essentially the language finally letting you have both at the same time — true encapsulation <em>and</em> prototype-based memory efficiency. That’s the problem the <code class="language-plaintext highlighter-rouge">class</code> keyword actually solved.</p>

<hr />

<h2 id="the-django-story-why-function-based-views-came-first">The Django Story: Why Function-Based Views Came First</h2>

<p>This same tension played out visibly in Django’s history — and it’s a useful real-world case study for the functions vs classes debate.</p>

<p>Django originally started with only function-based views because they are simple and explicit: a function receives an HTTP request, does something, returns a response. Clean, readable, no ceremony.</p>

<p>The problem wasn’t that functions were bad. The problem was <strong>repetition</strong>. A typical list view, a create view, a detail view — they all shared the same structural pattern: query the database, handle the request method, render a template, return a response. Writing that boilerplate from scratch for every view across a large application was tedious and error-prone.</p>

<p>Class-based views, introduced in Django 1.3, brought inheritance to address this. Subclass <code class="language-plaintext highlighter-rouge">ListView</code>, override a couple of attributes, get a fully working paginated list view in two lines.</p>

<p>But the criticism followed: the inheritance chains made implicit code flow harder to follow than the boilerplate they replaced. Django developers who preferred functions argued the complexity wasn’t a fair trade for the saved lines. It’s a debate that’s still active in the Django community today.</p>

<p>Django’s evolution mirrors the broader argument perfectly: functions are simpler and more readable, classes offer reusability through inheritance — but that reusability comes with cognitive overhead.</p>

<hr />

<h2 id="why-react-moved-from-class-components-to-functional-components">Why React Moved from Class Components to Functional Components</h2>

<p>React’s journey went in the opposite direction — starting with class components and moving back to functions — and it’s equally instructive.</p>

<p>Early React required class components for any component that needed state or lifecycle methods. Functions were only for purely presentational components that received props and rendered HTML.</p>

<p>The problems with React class components accumulated:</p>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">this</code> keyword behaved unpredictably — methods had to be manually bound in constructors</li>
  <li>Logic for the same feature was split across <code class="language-plaintext highlighter-rouge">componentDidMount</code>, <code class="language-plaintext highlighter-rouge">componentDidUpdate</code>, and <code class="language-plaintext highlighter-rouge">componentWillUnmount</code></li>
  <li>Sharing stateful logic between components required render props and higher-order components, creating deeply nested, hard-to-read trees</li>
</ul>

<p><strong>React Hooks</strong> solved this by bringing state and side effects into functions — without <code class="language-plaintext highlighter-rouge">this</code> binding issues and without splitting related logic across lifecycle methods. The same feature’s logic could now live together, in one plain function.</p>

<p>The shift wasn’t because functions are fundamentally superior to classes. It was because for UI components with localised, short-lived state, <strong>functions with Hooks are dramatically cleaner for this specific use case.</strong></p>

<hr />

<h2 id="when-to-use-functions-vs-classes-in-javascript-a-practical-guide">When to Use Functions vs Classes in JavaScript: A Practical Guide</h2>

<p>The answer isn’t a universal rule — it’s a set of questions to ask about what your code actually needs to do.</p>

<p><strong>Use a function when:</strong></p>
<ul>
  <li>The logic is stateless — input goes in, output comes out, no memory between calls</li>
  <li>You’re building UI components (functional components + hooks are the modern standard)</li>
  <li>You want maximum composability — small pieces of logic combining into larger ones</li>
  <li>You’re writing utility code, data transformations, or pure calculations</li>
</ul>

<p><strong>Use a class when:</strong></p>
<ul>
  <li>You need an entity that holds state across a long lifecycle — a database connection, an API client, a game object, a session</li>
  <li>You’ll create many instances of the same thing and memory efficiency matters</li>
  <li>You’re modelling a domain entity with behaviour — <code class="language-plaintext highlighter-rouge">ShoppingCart</code>, <code class="language-plaintext highlighter-rouge">UserSession</code>, <code class="language-plaintext highlighter-rouge">HttpClient</code></li>
  <li>You’re working in a framework built around classes — NestJS, for example, uses classes and decorators as its core architectural pattern</li>
</ul>

<p>The mental shortcut: <strong>functions for actions, classes for things.</strong></p>

<p>A function calculates tax, validates a form, fetches data. A class <em>is</em> a bank account, <em>is</em> an HTTP client, <em>is</em> a user session — something that persists, manages its own state, and has a lifecycle.</p>

<hr />

<h2 id="summary">Summary</h2>

<p>The industry’s move toward functions isn’t a rejection of classes — it’s a recognition that most day-to-day code is stateless logic and UI rendering, where functions genuinely are the simpler tool.</p>

<p>But when you need to create thousands of instances of something that holds state across a lifecycle, the prototype-based memory efficiency of classes is not a stylistic preference. It’s a practical necessity.</p>

<p>Functions and classes aren’t competing paradigms. They’re complementary tools — each optimised for a different problem. Knowing which problem you’re solving is the skill.</p>

<hr />

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<p><strong>What is the difference between a function and a class in JavaScript?</strong>
A function is a reusable block of code that takes inputs and returns outputs. A class is a blueprint for creating objects that bundle state and behaviour together. The key technical difference is that class methods are shared across all instances via the prototype chain, while closure-based factory functions create new method copies for each instance.</p>

<p><strong>When should you use a class instead of a function in JavaScript?</strong>
Use a class when you need to create multiple instances of something that maintains its own state across a lifecycle — like a database connection, API client, or game entity. Classes are more memory-efficient at scale because methods are defined once on the prototype and shared across all instances.</p>

<p><strong>Can JavaScript functions replace classes entirely?</strong>
Technically yes — closures can encapsulate private state, and constructor functions can use prototypes for memory efficiency. But ES6 classes with private fields (<code class="language-plaintext highlighter-rouge">#</code>) combine both benefits cleanly. For long-lived, stateful entities created at scale, classes remain the pragmatic choice.</p>

<p><strong>What is the JavaScript prototype chain?</strong>
The prototype chain is JavaScript’s mechanism for sharing properties and methods between objects. Class methods are defined once on the constructor’s prototype. When an instance calls a method, JavaScript looks it up on the prototype rather than the instance itself — meaning the method exists in memory exactly once regardless of how many instances exist.</p>

<p><strong>Why did React move from class components to functional components?</strong>
Class components required manual <code class="language-plaintext highlighter-rouge">this</code> binding, split related logic across multiple lifecycle methods, and made stateful logic reuse awkward. React Hooks brought state and side effects into functional components cleanly, eliminating <code class="language-plaintext highlighter-rouge">this</code> issues and keeping related logic together.</p>

<p><strong>Why did Django start with function-based views and add class-based views later?</strong>
Django started with function-based views for their simplicity and explicitness. Class-based views were added in Django 1.3 to reduce repetitive boilerplate through inheritance. However, many experienced Django developers argue the inheritance chains introduced more cognitive complexity than the boilerplate savings justified.</p>

<p><strong>What is the memory difference between closures and classes in JavaScript?</strong>
With closure-based factory functions, each instance gets its own copy of every method — creating duplicate function objects in memory. With classes, methods are defined once on the prototype and referenced by all instances. For thousands of instances, classes are significantly more memory-efficient.</p>

<hr />

<p><em>Co-written with AI.</em></p>]]></content><author><name>Samrat Dutta Roy</name></author><category term="javascript" /><category term="typescript" /><category term="functions vs classes" /><category term="closures" /><category term="prototype chain" /><category term="OOP" /><category term="functional programming" /><category term="React hooks" /><category term="class components" /><category term="Django views" /><category term="memory efficiency" /><category term="software architecture" /><category term="javascript interview" /><summary type="html"><![CDATA[Should you use functions or classes in JavaScript? Modern frameworks lean heavily on functions — React moved to hooks, Django started function-first. So why do classes still exist? The answer lies in memory, the prototype chain, and what your code actually needs to do.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dutta-roy-samrat.github.io/assets/images/functions-vs-classes.png" /><media:content medium="image" url="https://dutta-roy-samrat.github.io/assets/images/functions-vs-classes.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why Does Changing DNS Fix High Ping in Valorant? The Real Technical Reason</title><link href="https://dutta-roy-samrat.github.io/posts/why-does-changing-dns-fix-high-ping-in-valorant-the-real-technical-reason/" rel="alternate" type="text/html" title="Why Does Changing DNS Fix High Ping in Valorant? The Real Technical Reason" /><published>2026-03-15T00:00:00+00:00</published><updated>2026-03-15T00:00:00+00:00</updated><id>https://dutta-roy-samrat.github.io/posts/why-does-changing-dns-fix-high-ping-in-valorant-the-real-technical-reason</id><content type="html" xml:base="https://dutta-roy-samrat.github.io/posts/why-does-changing-dns-fix-high-ping-in-valorant-the-real-technical-reason/"><![CDATA[<blockquote>
  <p><strong>Direct answer:</strong> Switching DNS (e.g. from Google’s 8.8.8.8 to Cloudflare’s 1.1.1.1) can fix Valorant ping spikes because different DNS servers resolve the game domain to different regional server IPs. Changing the destination IP forces your ISP to build a fresh network route — one that may bypass a congested or broken path that was causing the spike. It doesn’t always work, and spikes can happen on any DNS at any time.</p>
</blockquote>

<p>If you’ve spent any time in gaming forums looking for ways to <strong>fix high ping in Valorant</strong>, you’ve probably seen this advice thrown around:</p>

<p><em>“Bro just switch to 8.8.8.8”</em> or <em>“No no, 1.1.1.1 is way better”</em></p>

<p>And the bizarre thing is — it sometimes works. Ping goes from spiking at 180ms to sitting clean at 40ms, just because you changed two numbers in your network settings.</p>

<p>I faced the exact same thing. I was mid-match in Valorant when my ping started spiking hard. Gunfights that should have been clean were getting me killed on my screen while my character was already dead on the server’s. Someone suggested <strong>switching DNS to Cloudflare — <code class="language-plaintext highlighter-rouge">1.1.1.1</code></strong>. I was sceptical. DNS is just a phonebook, what could it possibly have to do with in-game ping?</p>

<p>But I tried it. The spikes disappeared. Ping went stable.</p>

<p>From that point it became my go-to fix. But here’s the thing — it doesn’t always work. Sometimes I’d switch to Cloudflare and still get spikes. Sometimes the spikes would come back on Cloudflare an hour later. Sometimes switching back to Google fixed it, sometimes it didn’t.</p>

<p>That inconsistency is what pushed me to actually understand what’s happening. Because if you understand <em>why</em> it sometimes works, you also understand <em>why</em> it sometimes doesn’t.</p>

<p>The answer is far more interesting than “Cloudflare is faster.”</p>

<hr />

<h2 id="does-dns-actually-affect-ping-in-valorant">Does DNS Actually Affect Ping in Valorant?</h2>

<p>Before anything else — DNS itself doesn’t affect your in-game ping once you’re connected to a match. Let’s be clear about that.</p>

<p>DNS is a phonebook. You type <code class="language-plaintext highlighter-rouge">playvalorant.com</code>, your computer asks a DNS server <em>“what’s the IP address for this?”</em>, gets an answer, and then connects directly to that IP. The DNS server is out of the picture the moment it hands you the address.</p>

<p>So if DNS doesn’t sit in the middle of your game connection, why does switching it sometimes change your ping?</p>

<p>Because <strong>different DNS servers can give you different IP addresses for the same game.</strong></p>

<p>That one sentence is the entire answer. Everything below is explaining why.</p>

<hr />

<h2 id="1-what-is-anycast-routing-and-how-does-it-affect-valorant">1. What Is Anycast Routing and How Does It Affect Valorant?</h2>

<p>Most people picture <code class="language-plaintext highlighter-rouge">8.8.8.8</code> as a single Google server sitting somewhere. It isn’t.</p>

<p>Both use a technology called <strong>Anycast routing</strong>. With Anycast, hundreds of server nodes across the world all share the exact same IP address. When your computer sends a request to <code class="language-plaintext highlighter-rouge">8.8.8.8</code>, your ISP looks at your location and routes you to whichever Google node is geographically or topologically closest to you.</p>

<p>The same principle applies to Valorant’s game servers. Riot doesn’t run one server in one place — they have regional deployments across multiple data centers and infrastructure providers. Multiple IP addresses, multiple edge locations, all serving the same game.</p>

<p>So when you ask a DNS server <em>“where is Valorant?”</em>, it doesn’t just look up a single fixed answer. It figures out which regional deployment to point you to. And Google and Cloudflare make that decision differently — which brings us to the key technical reason behind all of this.</p>

<hr />

<h2 id="2-why-do-google-dns-and-cloudflare-give-different-ips-for-valorant">2. Why Do Google DNS and Cloudflare Give Different IPs for Valorant?</h2>

<p>Google DNS uses an extension called <strong>EDNS Client Subnet (ECS)</strong>. When you ask Google for Valorant’s server address, Google passes a masked version of your IP address to Riot’s authoritative DNS server. Riot’s system uses this to identify your approximate region and ISP, and returns the IP of the edge server most suitable for you — let’s call it <strong>Edge Server A</strong>.</p>

<p>Cloudflare, by contrast, is privacy-focused and <strong>deliberately does not support ECS</strong>. This is confirmed in Cloudflare’s own documentation — 1.1.1.1 does not send the EDNS Client Subnet header to authoritative servers, by design. When you ask Cloudflare the same question, Riot’s system doesn’t see your IP at all — it only sees Cloudflare’s Anycast node IP. Without location context, it makes a different routing decision and may return <strong>Edge Server B</strong> instead.</p>

<p>You aren’t hitting a proxy versus a main server. You aren’t getting a faster or slower version of Valorant. You’re simply being handed two different regional IP addresses — two different data center targets that both serve Valorant, but sit on completely different network paths from your machine.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>You → Google DNS  →  Riot sees your region via ECS   →  Returns IP for Edge Server A
You → Cloudflare  →  Riot sees Cloudflare's IP only  →  Returns IP for Edge Server B
</code></pre></div></div>

<hr />

<h2 id="3-how-does-switching-dns-actually-fix-a-ping-spike">3. How Does Switching DNS Actually Fix a Ping Spike?</h2>

<p>When your ping is spiking on Google DNS, it usually means the <strong>network path</strong> between your ISP and Edge Server A has become congested. Somewhere along the route — an overloaded internet exchange point, a saturated fiber link, a bad intermediate hop — packets are being delayed or dropped.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Your PC] --(Congested Path / Bad Node)--&gt; [Edge Server A]  ❌ High ping, packet loss
</code></pre></div></div>

<p>When you switch to Cloudflare, two things happen:</p>

<ol>
  <li>Cloudflare resolves Valorant’s domain to <strong>Edge Server B</strong> — a different IP, a different data center</li>
  <li>Your machine builds a fresh connection to this new destination, and your ISP routes you down a different network path to reach it</li>
</ol>

<p>Because Edge Server B is on a different network leg entirely, your traffic bypasses the broken, congested node that was causing the spikes.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Your PC] --(Clean, Uncongested Path)--&gt; [Edge Server B]  ✓ Stable ping
</code></pre></div></div>

<p>The DNS switch didn’t speed anything up. It rerouted you to a destination that happens to have a cleaner path from where you are right now.</p>

<hr />

<h2 id="4-why-does-switching-dns-not-always-fix-the-spike">4. Why Does Switching DNS Not Always Fix the Spike?</h2>

<p>This is the part most guides don’t explain — and it’s where the real picture gets honest.</p>

<p>Switching DNS is not a fix. It’s a gamble on whether the new network path is cleaner than the old one. And there’s no guarantee it will be.</p>

<p><strong>The new route can be just as bad.</strong> Edge Server B might be on a network path that’s also congested right now. You’ve just swapped one bad highway for another.</p>

<p><strong>The spike might not be routing at all.</strong> If the congestion is inside your ISP’s own network — between your modem and their core backbone — both DNS options share that segment. Switching DNS changes the destination, but not the local bottleneck.</p>

<p><strong>Riot’s servers can themselves be under load.</strong> If the edge server you’re resolving to is experiencing high player volume or infrastructure issues, your ping will spike regardless of which DNS handed you that IP.</p>

<p><strong>TTL expiration can change things mid-session.</strong> Every DNS record has a <strong>Time To Live</strong> — an expiration timer. When your cached Valorant IP expires, your DNS re-resolves the domain and may return a different server instance. If that new instance is under load, your ping spikes even though you didn’t change anything.</p>

<p>This is why the experience feels random. The DNS switch sometimes works because it happens to route you around a specific problem at that specific moment. It’s not a reliable fix — it’s a controlled re-roll of your network path.</p>

<hr />

<h2 id="the-full-picture-dns--ip--network-path--ping">The Full Picture: DNS → IP → Network Path → Ping</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. You query DNS  →  DNS returns an IP based on its routing logic (ECS or no ECS)
2. Your machine connects to that IP  →  ISP picks a network path to reach it
3. Ping is determined by  →  the quality of that path at that moment
4. Switching DNS  →  changes the destination IP  →  ISP builds a new path  →  may bypass congestion
</code></pre></div></div>

<p>DNS choice → destination IP → network path → ping.</p>

<p>None of this is about which DNS server is inherently “faster” or “better for gaming.” It’s about the knock-on effect of being handed a different destination, which forces a new route, which may or may not be cleaner than the previous one at that point in time.</p>

<hr />

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<p><strong>Does changing DNS reduce ping in Valorant?</strong>
Not directly. DNS doesn’t sit in the path of your in-game connection. But switching DNS can resolve Valorant to a different regional server IP, which forces your ISP to build a new network route. If the new route avoids congested or broken network paths, your ping may improve.</p>

<p><strong>Should I use Google DNS (8.8.8.8) or Cloudflare DNS (1.1.1.1) for gaming?</strong>
Neither is universally better. Google uses EDNS Client Subnet to route you based on your location, often giving you a geographically closer server. Cloudflare does not use ECS by design, which can result in a different — sometimes cleaner — network path. If one is causing spikes, switching to the other forces a re-route that may bypass the congestion.</p>

<p><strong>Why does my Valorant ping spike even after switching DNS?</strong>
Because the spike may not be caused by your network path to the game server. Congestion inside your ISP’s own network, high load on Riot’s servers, or TTL-triggered re-resolution to a loaded server instance can all cause spikes that DNS switching won’t fix.</p>

<p><strong>What is EDNS Client Subnet (ECS)?</strong>
EDNS Client Subnet is a DNS extension used by Google DNS that passes a masked version of your IP address to the authoritative DNS server. This allows services like Valorant to return a regionally appropriate server IP. Cloudflare deliberately does not implement ECS in order to protect user privacy.</p>

<p><strong>What is Anycast routing?</strong>
Anycast is a network addressing method where multiple servers share the same IP address. Requests to that IP are automatically routed to the nearest or most optimal node. Both Google DNS and Cloudflare DNS use Anycast, as do large-scale game server providers like Riot.</p>

<p><strong>Why does switching back to Google DNS sometimes fix the ping again?</strong>
Because network routing is dynamic. A route that was congested an hour ago may be clear now, and the route that was clean may now be saturated. Switching DNS forces your ISP to re-evaluate the path to a new destination — which at that moment may happen to be cleaner.</p>

<hr />

<p><em>Co-written with AI.</em></p>]]></content><author><name>Samrat Dutta Roy</name></author><category term="valorant high ping" /><category term="fix ping valorant" /><category term="dns ping fix" /><category term="cloudflare dns gaming" /><category term="google dns vs cloudflare" /><category term="anycast routing" /><category term="EDNS client subnet" /><category term="networking" /><category term="valorant lag fix" /><category term="dns 1.1.1.1 gaming" /><summary type="html"><![CDATA[Why does switching to Cloudflare DNS (1.1.1.1) or Google DNS (8.8.8.8) fix high ping in Valorant? It's not placebo — it's Anycast routing, EDNS Client Subnet, and how your ISP picks network paths. Here's the full technical explanation.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dutta-roy-samrat.github.io/assets/images/dns-ping-valorant.png" /><media:content medium="image" url="https://dutta-roy-samrat.github.io/assets/images/dns-ping-valorant.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">What is UTF-8 Character Encoding? A Real Bug That Finally Made It Click</title><link href="https://dutta-roy-samrat.github.io/posts/what-is-utf-8-character-encoding-a-real-bug-that-finally-made-it-click/" rel="alternate" type="text/html" title="What is UTF-8 Character Encoding? A Real Bug That Finally Made It Click" /><published>2026-02-18T00:00:00+00:00</published><updated>2026-02-18T00:00:00+00:00</updated><id>https://dutta-roy-samrat.github.io/posts/what-is-utf-8-character-encoding-a-real-bug-that-finally-made-it-click</id><content type="html" xml:base="https://dutta-roy-samrat.github.io/posts/what-is-utf-8-character-encoding-a-real-bug-that-finally-made-it-click/"><![CDATA[<blockquote>
  <p><strong>Direct answer:</strong> UTF-8 is a variable-width character encoding that converts Unicode code points into bytes. It uses 1 byte for ASCII characters, up to 4 bytes for emojis and rare symbols. It is the dominant encoding on the web because it is space-efficient, backward-compatible with ASCII, and can represent every character in existence.</p>
</blockquote>

<p>You’ve typed this line dozens of times without thinking about it:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>It sits at the top of every HTML file. Every tutorial tells you to put it there. Nobody really explains what <strong>UTF-8 character encoding</strong> actually means — or why it matters beyond copy-pasting it blindly.</p>

<p>I didn’t truly understand it until a bug involving emojis and a CSV download forced me to.</p>

<hr />

<h2 id="the-bug-that-started-this">The Bug That Started This</h2>

<p>I was adding an emoji picker to an existing message input feature. Nothing fancy — users type messages, optionally add an emoji, and the messages get stored. There was also an existing feature that let users download all messages as a CSV file, which they’d open in Excel.</p>

<p>The feature worked. Or so I thought.</p>

<p>When I tested it end-to-end, I noticed something strange in the downloaded CSV: <strong>some emojis displayed correctly in Excel, and some showed up as garbled nonsense</strong> — strange boxes, question marks, or completely wrong characters.</p>

<p>Same code. Same download function. Some emojis fine, some broken.</p>

<p>I had no idea where to start.</p>

<hr />

<h2 id="the-fix--and-the-questions-it-raised">The Fix — and the Questions It Raised</h2>

<p>The fix eventually came down to two small additions to the CSV download logic:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">BOM</span> <span class="o">=</span> <span class="nx">encodeDownloadFileInUTF8</span> <span class="p">?</span> <span class="p">[</span><span class="dl">"</span><span class="se">\</span><span class="s2">uFEFF</span><span class="dl">"</span><span class="p">]</span> <span class="p">:</span> <span class="p">[];</span>

<span class="kd">const</span> <span class="nx">csvBlob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([...</span><span class="nx">BOM</span><span class="p">,</span> <span class="nx">data</span> <span class="o">||</span> <span class="nx">blob</span><span class="p">],</span> <span class="p">{</span>
  <span class="na">type</span><span class="p">:</span> <span class="s2">`text/csv;</span><span class="p">${</span><span class="nx">encodeDownloadFileInUTF8</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">charset=utf-8;</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">""</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Two things were added:</p>
<ol>
  <li>A mysterious <code class="language-plaintext highlighter-rouge">"\uFEFF"</code> prepended to the file — called a <strong>BOM</strong></li>
  <li><code class="language-plaintext highlighter-rouge">charset=utf-8</code> added to the MIME type</li>
</ol>

<p>Both were added. The emojis rendered correctly in Excel.</p>

<p>But I refused to just move on without understanding <em>why</em>. What is a BOM? What does <code class="language-plaintext highlighter-rouge">charset=utf-8</code> actually tell the browser? And why were only <em>some</em> emojis broken in the first place?</p>

<p>Answering those questions meant going all the way back to basics — to how computers represent text at all.</p>

<hr />

<h2 id="how-does-a-computer-store-a-letter">How Does a Computer Store a Letter?</h2>

<p>Here’s something that seems obvious once you hear it, but nobody spells out clearly:</p>

<p><strong>Computers only understand numbers.</strong> Everything in memory — images, videos, code, text — is ultimately stored as binary digits: ones and zeros.</p>

<p>A number like <code class="language-plaintext highlighter-rouge">65</code> is easy. In binary it’s <code class="language-plaintext highlighter-rouge">01000001</code>. Done.</p>

<p>But what about the letter <code class="language-plaintext highlighter-rouge">"A"</code>? You can’t directly convert a character to binary the way you can a number. A character isn’t a number — it’s a symbol.</p>

<p>So how does it work?</p>

<p>The answer is: <strong>we agreed on a map.</strong></p>

<hr />

<h2 id="what-is-a-character-set-ascii-and-unicode-explained">What Is a Character Set? ASCII and Unicode Explained</h2>

<p>A <strong>character set</strong> (or charset) is essentially a lookup table. It maps every character — letters, digits, punctuation, symbols — to a unique number. Once a character has a number, that number can be converted to binary, and the machine can store it.</p>

<p>The two most important character sets:</p>

<p><strong>ASCII</strong> was one of the earliest and simplest. It maps 128 characters to numbers 0–127. The letter <code class="language-plaintext highlighter-rouge">"A"</code> is 65. <code class="language-plaintext highlighter-rouge">"B"</code> is 66. A space is 32. It covers the basic English alphabet, digits, and common punctuation — nothing more.</p>

<p><strong>Unicode</strong> is the modern, universal standard. Instead of 128 characters, it maps over 140,000 of them — every letter from every human language, mathematical symbols, and yes, emojis. The 😊 emoji, for example, is Unicode code point <code class="language-plaintext highlighter-rouge">U+1F60A</code>. In JavaScript, you can verify this:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">"</span><span class="s2">A</span><span class="dl">"</span><span class="p">.</span><span class="nx">charCodeAt</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>   <span class="c1">// 65  — the number Unicode (and ASCII) maps "A" to</span>
<span class="dl">"</span><span class="s2">😊</span><span class="dl">"</span><span class="p">.</span><span class="nx">codePointAt</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span> <span class="c1">// 128522 — the number Unicode maps this emoji to</span>
</code></pre></div></div>

<p>So the character set answers: <em>“What number does this character correspond to?”</em></p>

<p>But there’s a second question: <em>“How do we actually store that number in bytes?”</em></p>

<p>That’s where <strong>encoding</strong> comes in.</p>

<hr />

<h2 id="what-is-utf-8-and-how-does-it-work">What Is UTF-8 and How Does It Work?</h2>

<p><strong>UTF-8</strong> is not a character set. It’s an <strong>encoding</strong> — a set of rules for turning Unicode code point numbers into actual bytes that get written to memory or a file.</p>

<p>Here’s the key insight that makes UTF-8 clever:</p>

<ul>
  <li>Simple characters (basic English letters, digits) that have small code point numbers get stored in <strong>1 byte</strong></li>
  <li>Characters from other languages and accented letters use <strong>2 or 3 bytes</strong></li>
  <li>Emojis and rare symbols use <strong>4 bytes</strong></li>
</ul>

<p>This is what “variable-width encoding” means. UTF-8 uses as few bytes as necessary for each character.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"A"  → code point 65     → UTF-8: 1 byte  → 01000001
"é"  → code point 233    → UTF-8: 2 bytes → 11000011 10101001
"中" → code point 20013  → UTF-8: 3 bytes → 11100100 10111000 10001101
"😊" → code point 128522 → UTF-8: 4 bytes → 11110000 10011111 10011000 10001010
</code></pre></div></div>

<p>This is why UTF-8 became the dominant encoding for the web. It’s backward-compatible with ASCII (the first 128 characters encode identically), space-efficient for English text, and capable of representing every character in existence.</p>

<p>When you write <code class="language-plaintext highlighter-rouge">&lt;meta charset="UTF-8"&gt;</code> at the top of your HTML, you’re telling the browser: <em>“When you read this file’s bytes, decode them using UTF-8 rules.”</em></p>

<p>Without that declaration, the browser guesses. And when it guesses wrong, characters break.</p>

<hr />

<h2 id="why-were-only-some-emojis-breaking-in-the-csv">Why Were Only Some Emojis Breaking in the CSV?</h2>

<p>Now that the foundation is set, we can diagnose exactly what was happening.</p>

<p>The CSV was being generated and downloaded without explicitly declaring its encoding. Excel, when it opens a CSV, also has to guess the encoding. And Excel’s default assumption — especially on Windows — is not UTF-8. It often defaults to a legacy encoding like <strong>Windows-1252</strong>, which only handles 256 characters.</p>

<p>Here’s why only <em>some</em> emojis broke:</p>

<ul>
  <li>Simple emojis that happened to fall within the range Excel’s legacy encoding could interpret showed up fine — by accident</li>
  <li>Emojis with 4-byte UTF-8 sequences that fell completely outside the legacy encoding’s range came out as garbage</li>
</ul>

<p>It wasn’t a random bug. It was entirely predictable once you understand encoding.</p>

<hr />

<h2 id="the-fix-explained">The Fix, Explained</h2>

<p>Two things were added to the code. They look similar in purpose but operate at completely different layers — and only one of them actually fixed the bug.</p>

<h3 id="what-is-a-bom-byte-order-mark-and-why-does-it-fix-excel">What Is a BOM (Byte Order Mark) and Why Does It Fix Excel?</h3>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">BOM</span> <span class="o">=</span> <span class="nx">encodeDownloadFileInUTF8</span> <span class="p">?</span> <span class="p">[</span><span class="dl">"</span><span class="se">\</span><span class="s2">uFEFF</span><span class="dl">"</span><span class="p">]</span> <span class="p">:</span> <span class="p">[];</span>
<span class="kd">const</span> <span class="nx">csvBlob</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Blob</span><span class="p">([...</span><span class="nx">BOM</span><span class="p">,</span> <span class="nx">data</span> <span class="o">||</span> <span class="nx">blob</span><span class="p">],</span> <span class="p">{</span> <span class="p">...</span> <span class="p">});</span>
</code></pre></div></div>

<p><strong>BOM</strong> stands for <strong>Byte Order Mark</strong>. It’s a special invisible character (<code class="language-plaintext highlighter-rouge">U+FEFF</code>) prepended to the very start of a file.</p>

<p>To understand what the BOM actually is, you need a tiny bit of backstory.</p>

<p>When a number takes up multiple bytes in memory, there are two valid ways to arrange those bytes — most significant byte first (big-endian) or least significant byte first (little-endian). Both are legitimate. But if the reader doesn’t know which order was used, they’ll misread the number entirely.</p>

<p>UTF-16 and UTF-32 store characters in multi-byte chunks, so byte order matters for them. The character <code class="language-plaintext highlighter-rouge">U+FEFF</code> was placed at the very start of files so that readers could detect which byte order was used — if they read it as <code class="language-plaintext highlighter-rouge">FE FF</code> it’s big-endian, if they read it as <code class="language-plaintext highlighter-rouge">FF FE</code> it’s little-endian. That’s where the name <strong>Byte Order Mark</strong> comes from.</p>

<p><strong>UTF-8 doesn’t actually need this.</strong> It processes one byte at a time and builds up characters using its own rules, so byte order is never ambiguous. The BOM has no technical purpose in UTF-8.</p>

<p>But it got repurposed as a convention. When <code class="language-plaintext highlighter-rouge">\uFEFF</code> is encoded in UTF-8, it produces a specific three-byte sequence at the start of the file: <code class="language-plaintext highlighter-rouge">EF BB BF</code>. Excel on Windows recognises these exact bytes as a signal meaning <em>“this file is UTF-8.”</em> It has nothing to do with byte order anymore — Excel just looks for those three bytes, and if it finds them, it stops guessing and decodes the whole file as UTF-8.</p>

<p><strong>The BOM is what actually fixed the bug.</strong> It survives the journey from browser to disk to Excel because it lives inside the file itself — not in any header or metadata that gets stripped away.</p>

<h3 id="does-charsetutf-8-in-the-mime-type-fix-the-problem">Does <code class="language-plaintext highlighter-rouge">charset=utf-8</code> in the MIME Type Fix the Problem?</h3>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">type</span><span class="p">:</span> <span class="s2">`text/csv;charset=utf-8;`</span>
</code></pre></div></div>

<p>This tells the browser — and any application that reads MIME types — that the content is encoded in UTF-8.</p>

<p>But here’s the honest reality: <strong>for the specific bug we were fixing, this alone would not have helped.</strong></p>

<p>When a user downloads a CSV and opens it by double-clicking, Excel on Windows doesn’t see the MIME type. That information exists at the HTTP/browser layer and gets lost the moment the file is saved to disk. Excel never gets a chance to read it.</p>

<p>So why add it at all? Because not everything that reads your file is Excel:</p>

<ul>
  <li>A Python script processing the CSV programmatically will read the MIME type and decode correctly</li>
  <li>An API consuming the file will respect it</li>
  <li>A browser rendering the CSV directly will use it</li>
  <li>It documents intent — any tool that <em>does</em> respect MIME types will handle the file correctly</li>
</ul>

<p>Think of it this way:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th><code class="language-plaintext highlighter-rouge">charset=utf-8</code> in MIME</th>
      <th>BOM <code class="language-plaintext highlighter-rouge">\uFEFF</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Lives in</strong></td>
      <td>HTTP / MIME headers</td>
      <td>The file’s actual bytes</td>
    </tr>
    <tr>
      <td><strong>Survives saving to disk</strong></td>
      <td>❌ No</td>
      <td>✅ Yes</td>
    </tr>
    <tr>
      <td><strong>Excel on Windows reads it</strong></td>
      <td>❌ Not reliably</td>
      <td>✅ Yes</td>
    </tr>
    <tr>
      <td><strong>Programmatic tools read it</strong></td>
      <td>✅ Yes</td>
      <td>Mostly ignored</td>
    </tr>
  </tbody>
</table>

<p>The MIME type tells tools that <em>ask</em> what encoding this is. The BOM tells tools — like Excel — that <em>don’t bother asking.</em></p>

<p>For this bug, if you had to pick just one: the BOM was the fix. The <code class="language-plaintext highlighter-rouge">charset=utf-8</code> was the right thing to add alongside it — but it was good practice, not the cure.</p>

<hr />

<h2 id="the-mental-model-simplified">The Mental Model, Simplified</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Character  →  [Character Set]  →  Code Point Number  →  [Encoding]  →  Bytes in memory/file

   "A"     →    Unicode        →        65            →    UTF-8     →   01000001
   "😊"    →    Unicode        →      128522          →    UTF-8     →   4 bytes
</code></pre></div></div>

<ul>
  <li><strong>Character Set (Unicode):</strong> gives every symbol a unique number</li>
  <li><strong>Encoding (UTF-8):</strong> defines how to turn that number into actual bytes</li>
  <li><strong>BOM (<code class="language-plaintext highlighter-rouge">\uFEFF</code>):</strong> a signal baked into the file’s bytes — survives to disk, read by Excel</li>
  <li><strong><code class="language-plaintext highlighter-rouge">charset=utf-8</code>:</strong> a signal in the MIME/HTTP layer — read by browsers and programmatic tools, but lost when the file is saved locally</li>
</ul>

<hr />

<h2 id="what-to-take-away">What to Take Away</h2>

<p>The next time you see <code class="language-plaintext highlighter-rouge">&lt;meta charset="UTF-8"&gt;</code> or <code class="language-plaintext highlighter-rouge">charset=utf-8</code> in a content type header, you’ll know exactly what it’s doing. It’s not boilerplate. It’s a contract — you’re telling every tool in the chain: <em>“Here’s the encoding I used to write these bytes. Please use the same rules to read them.”</em></p>

<p>When that contract is missing or mismatched, text breaks. Sometimes obviously, sometimes only for edge cases like 4-byte emoji sequences that a legacy encoding never anticipated.</p>

<p>A small invisible character at the start of a file fixed my bug. Now I’ll never forget what it means.</p>

<hr />

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<p><strong>What is UTF-8 encoding?</strong>
UTF-8 is a variable-width character encoding system that converts Unicode code points into bytes. It uses 1 byte for basic ASCII characters and up to 4 bytes for emojis and rare symbols. It is the most widely used encoding on the web.</p>

<p><strong>What is the difference between a character set and an encoding?</strong>
A character set (like Unicode or ASCII) maps characters to unique numbers called code points. An encoding (like UTF-8) defines the rules for converting those code point numbers into actual bytes stored in memory or files. UTF-8 is an encoding of the Unicode character set.</p>

<p><strong>Why does <code class="language-plaintext highlighter-rouge">&lt;meta charset="UTF-8"&gt;</code> need to be in every HTML file?</strong>
Without it, the browser has to guess how to interpret the file’s bytes. If the guess is wrong, characters render as garbage — especially accented letters, non-Latin scripts, and emojis. Declaring <code class="language-plaintext highlighter-rouge">charset=utf-8</code> tells the browser exactly which rules to use.</p>

<p><strong>What is a Byte Order Mark (BOM) and when do you need it?</strong>
A BOM (<code class="language-plaintext highlighter-rouge">\uFEFF</code>) is a special invisible character prepended to a file’s bytes. In UTF-8 files, it encodes as <code class="language-plaintext highlighter-rouge">EF BB BF</code> and signals to applications like Excel on Windows that the file is UTF-8 encoded. It is particularly important when generating CSVs that users will open in Excel, since Excel does not reliably read the MIME type charset declaration after a file is saved to disk.</p>

<p><strong>Why do only some emojis break in Excel CSVs?</strong>
Excel on Windows defaults to a legacy encoding (like Windows-1252) when opening CSVs without explicit encoding signals. Emojis that happen to fall within that legacy encoding’s range display by accident. Emojis requiring 4-byte UTF-8 sequences — outside the legacy encoding’s range entirely — come out as garbled characters.</p>

<hr />

<p><em>Co-written with AI.</em></p>]]></content><author><name>Samrat Dutta Roy</name></author><category term="utf-8" /><category term="character encoding" /><category term="unicode" /><category term="javascript" /><category term="csv export" /><category term="byte order mark" /><category term="BOM" /><category term="charset" /><category term="emoji encoding" /><category term="frontend" /><category term="debugging" /><summary type="html"><![CDATA[What is UTF-8 encoding and why does it matter? A real emoji bug in a CSV export — and two lines of code that fixed it — explains character encoding, Unicode, the Byte Order Mark, and why charset=utf-8 alone isn't always enough.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dutta-roy-samrat.github.io/assets/images/utf8-encoding.webp" /><media:content medium="image" url="https://dutta-roy-samrat.github.io/assets/images/utf8-encoding.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Is Node.js Single-Threaded? The Complete Answer Explained</title><link href="https://dutta-roy-samrat.github.io/posts/is-nodejs-single-threaded-the-complete-answer-explained/" rel="alternate" type="text/html" title="Is Node.js Single-Threaded? The Complete Answer Explained" /><published>2026-01-20T00:00:00+00:00</published><updated>2026-01-20T00:00:00+00:00</updated><id>https://dutta-roy-samrat.github.io/posts/is-nodejs-single-threaded-the-complete-answer-explained</id><content type="html" xml:base="https://dutta-roy-samrat.github.io/posts/is-nodejs-single-threaded-the-complete-answer-explained/"><![CDATA[<blockquote>
  <p><strong>Direct answer:</strong> Node.js is neither purely single-threaded nor purely multi-threaded. JavaScript execution runs on a single main thread, but Node.js as a runtime uses multiple background threads via libuv for I/O operations. Worker Threads and the Cluster Module enable true parallelism when needed.</p>
</blockquote>

<p>“Is Node.js single-threaded?”</p>

<p>It’s one of those <strong>Node.js interview questions</strong> that sounds straightforward. You’ve probably heard the answer thrown around confidently in videos, tutorials, and bootcamps — <em>“Yes! Node.js is single-threaded. That’s its whole thing.”</em></p>

<p>Except that’s not the full picture. And in an interview — or in production — giving that answer will cost you.</p>

<p>Answer “yes” — you’re wrong. Answer “no” — also wrong.</p>

<p>The accurate answer is: <strong>JavaScript execution in Node.js is single-threaded, but Node.js as a runtime environment is multi-threaded.</strong></p>

<p>That single sentence is the difference between someone who has used Node.js and someone who truly understands the <strong>Node.js architecture</strong>. Let’s break it down — slowly, with no hand-waving.</p>

<hr />

<h2 id="what-is-a-thread-and-why-does-it-matter-in-nodejs">What Is a Thread and Why Does It Matter in Node.js?</h2>

<p>Before we even get to Node.js, it helps to understand what a thread actually is.</p>

<p>Think of your CPU as a kitchen. A <strong>thread</strong> is a single chef working in that kitchen. One chef can only do one thing at a time — chop vegetables, stir a pot, or plate a dish. If you have multiple chefs (multiple threads), they can work on different things simultaneously.</p>

<p>Your operating system manages hundreds of threads across all your running applications. When you open Chrome, it spins up multiple threads. When you run a Node.js server, it also creates threads — more than most people realise.</p>

<p>With that foundation set, let’s look at what actually happens when Node.js starts up.</p>

<hr />

<h2 id="1-why-is-javascript-in-nodejs-single-threaded">1. Why Is JavaScript in Node.js Single-Threaded?</h2>

<p>When you write JavaScript in Node.js — your route handlers, your business logic, your <code class="language-plaintext highlighter-rouge">async/await</code> functions — all of it runs on a single <strong>main thread</strong>.</p>

<p>This means:</p>

<ul>
  <li>There is only one <strong>Call Stack</strong></li>
  <li>Only one line of your JavaScript executes at any given moment</li>
  <li>This main thread runs the <strong>Event Loop</strong>, which is the heartbeat of every Node.js application</li>
</ul>

<p>The Event Loop is what gives Node.js its reputation for handling many requests efficiently. It constantly checks: <em>“Is there something in the queue waiting to run? Is the Call Stack empty? Okay, let’s go.”</em></p>

<p>But here’s the thing junior developers often don’t fully internalise: <strong>if you block the main thread, you block everything.</strong></p>

<p>Write a synchronous calculation that takes 10 seconds:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Don't ever do this in a server</span>
<span class="kd">const</span> <span class="nx">startTime</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>
<span class="k">while</span> <span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">startTime</span> <span class="o">&lt;</span> <span class="mi">10000</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// burning 10 seconds of CPU</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Your entire server is frozen for those 10 seconds. Every user who sends a request during that time gets silence. The Event Loop cannot move. No callbacks run. No responses go out. One bad piece of synchronous code and your whole server becomes unresponsive.</p>

<p>This is called <strong>blocking the Event Loop</strong> — and it’s the most common and painful Node.js performance mistake.</p>

<hr />

<h2 id="2-how-does-nodejs-handle-multiple-requests-without-freezing">2. How Does Node.js Handle Multiple Requests Without Freezing?</h2>

<p>If JavaScript is single-threaded, how does a Node.js server handle reading files, querying a database, hashing passwords, and responding to hundreds of users — all seemingly at the same time — without freezing?</p>

<p>The answer: <strong>Node.js doesn’t do all of that on the main thread.</strong></p>

<p>Node.js is built on top of two core components: Google’s <strong>V8 engine</strong> (which executes your JavaScript) and a powerful C++ library called <strong>libuv</strong> (which handles everything that touches the operating system).</p>

<p>When you ask Node.js to do something slow and I/O-heavy — reading a file from disk, making a network call, hashing a password with bcrypt — it doesn’t sit and wait. It hands that task to <strong>libuv</strong>, which manages a background <strong>Thread Pool</strong>.</p>

<p>By default, that thread pool has <strong>4 threads</strong>. These threads are invisible to you as a developer. You never write code for them directly. But they are always there, quietly doing the heavy lifting.</p>

<p>Here’s exactly what happens, step by step, when you call <code class="language-plaintext highlighter-rouge">fs.readFile</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Your code calls fs.readFile() on the main thread
          │
          ▼
2. Main thread hands the task to libuv thread pool
   (main thread immediately moves on — it doesn't wait)
          │
          ▼
3. A background thread picks up the task
   and asks the OS to retrieve the file from disk
          │
          ▼
4. OS returns the file data to the background thread
          │
          ▼
5. Background thread pushes a callback into the Callback Queue
          │
          ▼
6. Event Loop sees the Call Stack is empty,
   picks up the callback, and runs it on the main thread
</code></pre></div></div>

<p>The main thread never sat idle waiting for the file. It went off and served other requests. When the file was ready, the result came back to it through the queue. That’s the magic.</p>

<hr />

<h2 id="the-waiter-analogy-this-will-stick-with-you">The Waiter Analogy (This Will Stick With You)</h2>

<p>Think of a Node.js server as a restaurant with a single, highly efficient waiter.</p>

<p>The waiter (main thread) takes your order and immediately walks it to the kitchen pass-through. They don’t go into the kitchen and personally cook your food. They don’t stand at the pass-through waiting either. They spin around and take the next table’s order.</p>

<p>The kitchen staff (libuv background threads) handle all the time-consuming cooking. When your food is ready, they ring a bell. The waiter picks it up and brings it to your table.</p>

<p>This is exactly how Node.js handles I/O concurrency. One waiter, moving fast, backed by a kitchen full of workers. The waiter is never blocked — unless someone forces them to personally cook a steak right at the front desk. That’s your <code class="language-plaintext highlighter-rouge">while</code> loop.</p>

<hr />

<h2 id="3-can-nodejs-run-javascript-in-parallel-worker-threads-explained">3. Can Node.js Run JavaScript in Parallel? Worker Threads Explained</h2>

<p>libuv handles system-level I/O in the background automatically. But what if your bottleneck isn’t file reading or database queries — what if it’s pure, heavy JavaScript computation?</p>

<p>Imagine you’re building a feature that processes video frames, runs a machine learning model, or crunches a massive dataset. These are CPU-bound tasks. libuv won’t help you here because the work itself happens in JavaScript — on the main thread.</p>

<p>For this, Node.js gives you two native tools:</p>

<h3 id="what-are-worker-threads-in-nodejs">What Are Worker Threads in Node.js?</h3>

<p>The <code class="language-plaintext highlighter-rouge">worker_threads</code> module lets you spin up a completely separate V8 engine and Call Stack inside your process. You can send the heavy computation to a Worker, and the operating system will schedule that Worker onto a separate, idle CPU core.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">Worker</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">worker_threads</span><span class="dl">'</span><span class="p">);</span>

<span class="c1">// Offload heavy work to a Worker</span>
<span class="kd">const</span> <span class="nx">worker</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Worker</span><span class="p">(</span><span class="dl">'</span><span class="s1">./heavy-calculation.js</span><span class="dl">'</span><span class="p">);</span>

<span class="nx">worker</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">message</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">result</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Got result:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">result</span><span class="p">);</span>
  <span class="c1">// Main thread was free this entire time</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Your main thread stays at 0% utilisation and keeps serving web traffic while the Worker grinds through the computation on another core. Workers in the same process can even share raw memory directly using <code class="language-plaintext highlighter-rouge">SharedArrayBuffer</code> — no costly data copying between threads.</p>

<h3 id="what-does-the-nodejs-cluster-module-do">What Does the Node.js Cluster Module Do?</h3>

<p>While Worker Threads add parallelism inside a single Node.js process, the <strong>Cluster Module</strong> goes a level higher — it duplicates your entire application across multiple CPU cores.</p>

<p>If you have an 8-core machine, Cluster forks your application 8 times. You now have 8 completely independent Node.js processes, each with its own memory and Event Loop. A master process sits at your server port and distributes incoming requests across all 8 using round-robin load balancing.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">cluster</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">cluster</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">os</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">os</span><span class="dl">'</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">cluster</span><span class="p">.</span><span class="nx">isMaster</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">totalCPUs</span> <span class="o">=</span> <span class="nx">os</span><span class="p">.</span><span class="nx">cpus</span><span class="p">().</span><span class="nx">length</span><span class="p">;</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">totalCPUs</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">cluster</span><span class="p">.</span><span class="nx">fork</span><span class="p">();</span> <span class="c1">// Spawn one process per core</span>
  <span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="c1">// Each worker runs your actual server</span>
  <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./server</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If one instance crashes, the other 7 keep running. The master process can restart the crashed worker automatically.</p>

<hr />

<h2 id="4-the-thread-multiplication-trap-that-breaks-production-servers">4. The Thread Multiplication Trap That Breaks Production Servers</h2>

<p>Here’s something that even experienced engineers get wrong when they first combine these two tools.</p>

<p>Say you have an <strong>8-core server</strong>. You use Cluster to spawn 8 application instances — one per core, perfectly sensible. You also have a heavy data-processing feature, so inside your application code you configure a Worker Thread pool of 4 threads to handle it.</p>

<p>Feels reasonable, right? Let’s do the math.</p>

<p>Cluster duplicates your entire application — including your Worker Thread configuration. So you haven’t created 4 background threads. You’ve created:</p>

<p><strong>8 Cluster Instances × 4 Worker Threads = 32 Active Threads</strong></p>

<p>32 software threads are now fighting over 8 physical CPU cores. The operating system has to constantly pause threads, save their state to memory, load another thread’s state, and resume — a process called <strong>context switching</strong>. It happens so fast it looks simultaneous to a human, but it is expensive. If you overwhelm your hardware, the CPU spends more time switching between threads than actually running your code.</p>

<p>Your server grinds to a halt — not because of bad code, but because of bad thread arithmetic.</p>

<p><strong>The rule to remember:</strong> <code class="language-plaintext highlighter-rouge">Total Cluster Instances + Total Worker Threads ≤ Total Physical Cores</code></p>

<p>On an 8-core machine: run 4 Cluster instances, and cap your Worker pool at 1 thread per instance. Leave breathing room.</p>

<hr />

<h2 id="5-nodejs-in-production-pm2-vs-aws-load-balancer">5. Node.js in Production: PM2 vs AWS Load Balancer</h2>

<p>Understanding Node.js threading isn’t just interview trivia — it directly shapes how you architect systems at scale.</p>

<h3 id="what-is-pm2-and-when-should-you-use-it">What Is PM2 and When Should You Use It?</h3>

<p><strong>PM2</strong> is a tool you install on a server that manages your Node.js processes. It handles the Cluster Module automatically (<code class="language-plaintext highlighter-rouge">pm2 start app.js -i max</code> spawns one process per CPU core), restarts crashed instances in milliseconds, and keeps your app alive through server reboots.</p>

<p>It’s the go-to solution for a single VPS or bare-metal Linux server.</p>

<h3 id="pm2-vs-aws-application-load-balancer-whats-the-difference">PM2 vs AWS Application Load Balancer: What’s the Difference?</h3>

<p>An <strong>AWS ALB</strong> operates at a completely different level. It doesn’t know or care about threads or cores inside your application. Its job is to distribute incoming internet traffic across a fleet of completely separate machines or containers.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>PM2</th>
      <th>AWS Load Balancer</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Scales</strong></td>
      <td>Inside one machine</td>
      <td>Across multiple machines</td>
    </tr>
    <tr>
      <td><strong>Best for</strong></td>
      <td>Single VPS / Linux server</td>
      <td>Cloud / Docker / Kubernetes</td>
    </tr>
    <tr>
      <td><strong>If the host dies</strong></td>
      <td>Everything goes offline</td>
      <td>Traffic reroutes to healthy machines</td>
    </tr>
    <tr>
      <td><strong>Setup complexity</strong></td>
      <td>Low</td>
      <td>Requires stateless architecture</td>
    </tr>
  </tbody>
</table>

<p>In modern production systems, senior engineers don’t pick one — they use both layers together:</p>

<ol>
  <li>Package the Node.js app in a <strong>Docker container</strong>, configured as a single-threaded instance on exactly 1 virtual CPU core</li>
  <li>Use <strong>AWS ECS or Kubernetes</strong> to run dozens of identical containers</li>
  <li>Put an <strong>AWS Application Load Balancer</strong> in front of the entire fleet</li>
</ol>

<p>One single-threaded instance per container. No manual thread management in code. The infrastructure scales horizontally, and Node.js does what it’s best at — handling I/O concurrency on a single, fast, non-blocking event loop.</p>

<hr />

<h2 id="summary-nodejs-threading-model-at-a-glance">Summary: Node.js Threading Model at a Glance</h2>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Threading</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Your JavaScript code</td>
      <td>Single-threaded (main thread + Event Loop)</td>
    </tr>
    <tr>
      <td>libuv I/O operations</td>
      <td>Multi-threaded (4-thread pool, invisible to you)</td>
    </tr>
    <tr>
      <td>Worker Threads</td>
      <td>Multi-threaded (you control it explicitly)</td>
    </tr>
    <tr>
      <td>Cluster Module</td>
      <td>Multi-process (one full app per CPU core)</td>
    </tr>
  </tbody>
</table>

<p>Node.js is not single-threaded. It is not multi-threaded. It is a carefully designed runtime that uses a single thread for your code, and an intelligent background system for everything else — so that one fast, non-blocking thread can do the work of many.</p>

<p>That’s the real answer. And now you know why it’s a trick question.</p>

<hr />

<h2 id="frequently-asked-questions">Frequently Asked Questions</h2>

<p><strong>Is Node.js single-threaded or multi-threaded?</strong>
Node.js JavaScript execution is single-threaded — one Call Stack, one Event Loop. But the Node.js runtime uses a multi-threaded background thread pool (via libuv) for I/O operations like file reads, database queries, and cryptography.</p>

<p><strong>What is the Node.js Event Loop?</strong>
The Event Loop is the mechanism that allows Node.js to perform non-blocking I/O despite running JavaScript on a single thread. It continuously checks the Callback Queue and executes pending callbacks when the Call Stack is empty.</p>

<p><strong>What is libuv in Node.js?</strong>
libuv is a C++ library that Node.js uses to handle asynchronous I/O operations. It maintains a background thread pool of 8 threads by default, which handle tasks like file system access and cryptographic functions without blocking the main JavaScript thread.</p>

<p><strong>When should you use Worker Threads vs the Cluster Module?</strong>
Use Worker Threads for CPU-bound tasks (heavy computation) within a single application process. Use the Cluster Module to scale across multiple CPU cores by running independent copies of your entire application. Avoid combining them without careful thread budgeting.</p>

<p><strong>What does “blocking the Event Loop” mean?</strong>
Blocking the Event Loop means running a synchronous operation on the main thread that takes a long time to complete — like a heavy <code class="language-plaintext highlighter-rouge">while</code> loop or synchronous file read. While it runs, Node.js cannot process any other requests, effectively freezing the server.</p>

<hr />

<p><em>Co-written with AI.</em></p>]]></content><author><name>Samrat Dutta Roy</name></author><category term="nodejs" /><category term="single-threaded" /><category term="event loop" /><category term="libuv" /><category term="worker threads" /><category term="cluster module" /><category term="backend" /><category term="javascript" /><category term="node.js architecture" /><category term="interview questions" /><summary type="html"><![CDATA[Is Node.js single-threaded or multi-threaded? The real answer is both — and understanding why will change how you write backend code. A deep dive into the Event Loop, libuv thread pool, Worker Threads, and Cluster Module.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://dutta-roy-samrat.github.io/assets/images/nodejs-threading.webp" /><media:content medium="image" url="https://dutta-roy-samrat.github.io/assets/images/nodejs-threading.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>