<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ workers - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ workers - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 21 Sep 2026 05:10:09 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/workers/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ High-Frequency Real-Time Data in React: From Ring Buffers to OffscreenCanvas ]]>
                </title>
                <description>
                    <![CDATA[ React is great at many things. But if you've ever tried pushing thousands of data points per second through it, you'll quickly learn that React isn't a firehose. It's more like a garden hose. Try forc ]]>
                </description>
                <link>https://www.freecodecamp.org/news/high-frequency-real-time-data-in-react-from-ring-buffers-to-offscreencanvas/</link>
                <guid isPermaLink="false">6a84d268fa401f6597cd200e</guid>
                
                    <category>
                        <![CDATA[ react js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ performance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ canvas ]]>
                    </category>
                
                    <category>
                        <![CDATA[ realtime ]]>
                    </category>
                
                    <category>
                        <![CDATA[ workers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ concurrency ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vineeth Pawar ]]>
                </dc:creator>
                <pubDate>Tue, 18 Aug 2026 21:45:12 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c3ffaae8-51d5-4add-9a9e-e49443e49746.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>React is great at many things. But if you've ever tried pushing thousands of data points per second through it, you'll quickly learn that React isn't a firehose. It's more like a garden hose.</p>
<p>Try forcing too much through it, and either the lawn floods (your DOM) or the pipe bursts (your app).</p>
<p>There's a second observation that pairs with the first. Your laptop has 8 to 16 CPU cores. Your React app uses 1 of them, almost always. The main thread handles JavaScript, the DOM, layout, and paint setup. The other cores sit idle while the main thread struggles to keep a 60fps frame budget.</p>
<p>Both problems have the same shape: you need to keep React out of the hot path, and you need to use more than one thread. The patterns that get you there also happen to be the patterns behind Figma's canvas engine, Bloomberg's trading dashboards, and every biosignal viewer you've seen.</p>
<p>In one project, I had to visualise 19 EEG (brainwave) channels, each sending about 1,000 data points per second. That's almost 19,000 updates per second. If you feed all of that directly into React, the UI doesn't just slow down. It faints dramatically.</p>
<p>This article is the end-to-end architecture I landed on: the ring buffers, workers, shared memory, off-main rendering, and specific patterns that hold up under sustained multi-hour load.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-whos-already-doing-this">Who's Already Doing This?</a></p>
</li>
<li><p><a href="#heading-the-1khz-math">The 1kHz Math</a></p>
</li>
<li><p><a href="#heading-where-it-usually-goes-wrong">Where it Usually Goes Wrong</a></p>
</li>
<li><p><a href="#heading-the-mental-model-air-traffic-control-plus-a-kitchen-brigade">The Mental Model: Air Traffic Control Plus a Kitchen Brigade</a></p>
</li>
<li><p><a href="#heading-step-1-stop-putting-samples-in-react-state">Step 1: Stop Putting Samples in React State</a></p>
</li>
<li><p><a href="#heading-step-2-separate-shape-from-values">Step 2: Separate Shape from Values</a></p>
</li>
<li><p><a href="#heading-step-3-move-heavy-work-off-the-main-thread">Step 3: Move Heavy Work Off the Main Thread</a></p>
</li>
<li><p><a href="#heading-step-4-render-off-main-with-offscreencanvas">Step 4: Render Off Main with OffscreenCanvas</a></p>
</li>
<li><p><a href="#heading-step-5-decimate-before-you-draw">Step 5: Decimate Before You Draw</a></p>
</li>
<li><p><a href="#heading-step-6-wrap-an-external-renderer">Step 6: Wrap an External Renderer</a></p>
</li>
<li><p><a href="#heading-step-7-when-canvas-isnt-enough-reach-for-webgl">Step 7: When Canvas isn't Enough, Reach for WebGL</a></p>
</li>
<li><p><a href="#heading-step-8-keep-memory-flat">Step 8: Keep Memory Flat</a></p>
</li>
<li><p><a href="#heading-step-9-scheduling-strategies">Step 9: Scheduling Strategies</a></p>
</li>
<li><p><a href="#heading-step-10-measure-sustained-performance">Step 10: Measure Sustained Performance</a></p>
</li>
<li><p><a href="#heading-case-study-19-eeg-channels">Case Study: 19 EEG Channels</a></p>
</li>
<li><p><a href="#heading-benchmarks-single-thread-vs-multi-thread">Benchmarks: Single Thread vs Multi-Thread</a></p>
</li>
<li><p><a href="#heading-the-coopcoep-catch">The COOP/COEP Catch</a></p>
</li>
<li><p><a href="#heading-production-tradeoffs">Production Tradeoffs</a></p>
</li>
<li><p><a href="#heading-should-you-build-like-this">Should You Build Like This?</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the most out of this article, you'll want:</p>
<ul>
<li><p><strong>Working knowledge of React 18 or 19.</strong> You should be comfortable with <code>useState</code>, <code>useEffect</code>, <code>useRef</code>, and the difference between mounting and re-rendering.</p>
</li>
<li><p><strong>TypeScript basics.</strong> Most examples are in TypeScript. You should be able to read type annotations without stopping.</p>
</li>
<li><p><strong>A rough sense of the browser main thread and event loop.</strong> You don't need to have written a Web Worker, but knowing what "blocking the main thread" means will make Step 3 easier.</p>
</li>
<li><p><strong>Familiarity with Canvas 2D or a chart library</strong> is a plus, not a requirement. If you've drawn anything on a canvas, you're ready.</p>
</li>
<li><p><strong>A laptop that can run modern Chrome or Edge.</strong> The examples rely on <code>SharedArrayBuffer</code>, <code>OffscreenCanvas</code>, and Atomics, which need a Chromium-based browser and cross-origin isolation (covered later in the article).</p>
</li>
</ul>
<p>You don't need prior experience with Web Workers, ring buffers, or WebGL. This article introduces each in the context of a real problem.</p>
<h2 id="heading-whos-already-doing-this">Who's Already Doing This?</h2>
<p>The patterns in this article aren't experimental. They're the architecture behind production apps that ingest and render high-frequency data:</p>
<ul>
<li><p><strong>Trading and finance dashboards</strong> (Bloomberg, Hyperliquid, dYdX, every serious market viewer) push thousands of price ticks per second through canvas-rendered grids.</p>
</li>
<li><p><strong>Figma</strong> runs its entire canvas engine in WebAssembly inside a worker. The main thread renders React for the chrome only.</p>
</li>
<li><p><strong>Google Docs and Microsoft Loop</strong> run their document models in workers, with the DOM as the projection.</p>
</li>
<li><p><strong>Charting libraries</strong> like LightningChart, uPlot, Plotly, and ECharts draw on Canvas or WebGL and treat React as a wrapper.</p>
</li>
<li><p><strong>Biosignal, ECG, EEG, and motion-capture apps</strong> routinely process samples at 1kHz or higher and stream them to live plots.</p>
</li>
<li><p><strong>Observability and APM tools</strong> (Datadog live tail, Grafana real-time panels) decouple ingestion from render to keep tabs responsive.</p>
</li>
<li><p><strong>Audio editors and visualisers</strong>, plus anything using the Web Audio API with a waveform display.</p>
</li>
<li><p><strong>transformers.js and ONNX Runtime Web</strong> place ML inference in workers by default.</p>
</li>
</ul>
<p>Different domains, same trick: React owns what changes rarely, something else owns what changes at refresh rate, and heavy work happens on threads that aren't the main one.</p>
<h2 id="heading-the-1khz-math">The 1kHz Math</h2>
<p>Some numbers to make the problem concrete.</p>
<ul>
<li><p>A sample arrives every 1ms.</p>
</li>
<li><p>A 60Hz display refreshes every 16.67ms.</p>
</li>
<li><p>So in one frame, you'll receive roughly <strong>16 to 17 samples per stream</strong>.</p>
</li>
<li><p>With 19 active streams (the EEG case), that's <strong>300 to 320 samples per frame</strong>.</p>
</li>
</ul>
<p>If you <code>setState</code> on each sample, React tries to do around 19,000 renders per second. It can't, so it skips frames. The UI stutters and your laptop fans take off.</p>
<p>If you <code>setState</code> once per frame with the batch of ~320 samples, React does 60 renders per second, which is easy.</p>
<p>That single reframe is the entire trick, and it will echo through every step below.</p>
<h2 id="heading-where-it-usually-goes-wrong">Where it Usually Goes Wrong</h2>
<p>Here's the version of the code I see in most real-time React apps the first time they try this. It looks reasonable. It's also the source of every bit of jank the team will spend the next two weeks tracking down.</p>
<pre><code class="language-tsx">import { useEffect, useState } from "react";

export default function NaiveChart({ socket }) {
  const [data, setData] = useState&lt;number[]&gt;([]);

  useEffect(() =&gt; {
    socket.on("newPoint", (point: number) =&gt; {
      setData((prev) =&gt; [...prev, point]); // re-renders every time
    });
  }, [socket]);

  return &lt;div&gt;{data.length} points&lt;/div&gt;;
}
</code></pre>
<p>Three problems baked into seven lines:</p>
<ol>
<li><p><strong>Every incoming sample triggers a re-render:</strong> At 1kHz that's 1,000 renders per second. React was never going to be happy about that.</p>
</li>
<li><p><code>[...prev, point]</code> <strong>allocates a new array on every push:</strong> At 1kHz that's a new array per millisecond, all of which the garbage collector has to clean up. The heap climbs, GC pauses lengthen, the fan kicks in.</p>
</li>
<li><p><strong>There's no upper bound on the array:</strong> Run this for an hour and you have 3.6 million numbers in memory, all of which React has to consider on every render.</p>
</li>
</ol>
<p>The fix isn't one trick. It's a stack of small ones, each addressing one of those failure modes and, eventually, the deeper problem of the main thread being the only thread.</p>
<h2 id="heading-the-mental-model-air-traffic-control-plus-a-kitchen-brigade">The Mental Model: Air Traffic Control Plus a Kitchen Brigade</h2>
<p>Before the code, two metaphors that will keep the pieces straight.</p>
<p><strong>First, air traffic control:</strong> Three roles, one airport.</p>
<ul>
<li><p>The <strong>control tower</strong> (your store) sees every plane (every sample) and tracks where it is.</p>
</li>
<li><p>The <strong>ground crew</strong> (React) sets up the runways and gates: the layout that planes use.</p>
</li>
<li><p>The <strong>pilots</strong> (the draw loop) actually fly. They look at the tower for clearance and act every few seconds.</p>
</li>
</ul>
<p>The control tower doesn't pull a gate from the ground crew every time a plane moves. It just holds the data. The ground crew rearranges gates when the schedule changes, which is rare. The pilots act constantly, at their own rate, off the tower's data.</p>
<p><strong>Second, a kitchen brigade:</strong> A restaurant kitchen at peak hour. One head chef can't make every dish. The classical brigade has stations: sauce, fish, pastry, garde manger, plating, and service. Each station owns a slice of the meal. The expediter coordinates timing.</p>
<ul>
<li><p>The <strong>expediter</strong> is your main thread.</p>
</li>
<li><p>The <strong>stations</strong> are your workers.</p>
</li>
<li><p>The <strong>plating window</strong> is your shared memory.</p>
</li>
<li><p>The <strong>dishes going to tables</strong> are your rendered frames.</p>
</li>
</ul>
<p>A single chef trying to do everything serially is your main-thread-only frontend. A brigade is your worker-based one. The brigade is faster because cuts, sauces, and sears happen in parallel, not because any one cook is faster than the soloist. The expediter doesn't cook, they orchestrate.</p>
<p>Everything below is a specific application of these two ideas.</p>
<h2 id="heading-step-1-stop-putting-samples-in-react-state">Step 1: Stop Putting Samples in React State</h2>
<p>The single biggest mistake in real-time React apps is treating every sample as state, which it isn't. State is what determines <em>which components exist and how they're arranged</em>. A live plot is one component. The 60,000 samples scrolling across it aren't 60,000 pieces of state. They're one buffer.</p>
<p>You want a store that lives outside React. A ring buffer over a typed array gives you constant-time inserts and a bounded heap:</p>
<pre><code class="language-ts">type Listener = () =&gt; void;

export function createSampleStore(capacity: number) {
  const buf = new Float32Array(capacity);
  let head = 0;
  let size = 0;
  const listeners = new Set&lt;Listener&gt;();

  return {
    push(sample: number) {
      buf[head] = sample;
      head = (head + 1) % capacity;
      if (size &lt; capacity) size++;
    },
    pushBatch(samples: Float32Array) {
      for (let i = 0; i &lt; samples.length; i++) {
        buf[head] = samples[i];
        head = (head + 1) % capacity;
        if (size &lt; capacity) size++;
      }
    },
    read(): Float32Array {
      if (size &lt; capacity) return buf.subarray(0, size);
      const out = new Float32Array(capacity);
      out.set(buf.subarray(head));
      out.set(buf.subarray(0, head), capacity - head);
      return out;
    },
    subscribe(l: Listener) {
      listeners.add(l);
      return () =&gt; listeners.delete(l);
    },
  };
}
</code></pre>
<p>Notice what's not here: no <code>useState</code>, no setter, no React anything. It's just plain JavaScript. The store can accept a million pushes per second and React won't care, because React isn't subscribed.</p>
<p>Here are the ring buffer mechanics, visualised:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779361162753/2374fc6e-429c-4a8a-8b31-30fb51c6052c.png" alt="Ring buffer with eight indexed slots, a head pointer advancing each push and wrapping around when it reaches the end, providing constant-time inserts and a bounded heap" style="display: block;" width="1600" height="463" loading="lazy">

<p>We have head advances on every push and wraps at capacity. Reads pull a window of the last N samples in chronological order. The result is constant time and a bounded heap.</p>
<p>A simpler stop-gap, when you don't want a typed buffer yet, is to put the rolling window in a ref instead of state:</p>
<pre><code class="language-tsx">import { useEffect, useRef } from "react";

export default function RefChart({ socket }) {
  const bufferRef = useRef&lt;number[]&gt;([]);

  useEffect(() =&gt; {
    socket.on("newPoint", (point: number) =&gt; {
      bufferRef.current.push(point);
      if (bufferRef.current.length &gt; 1000) {
        bufferRef.current.shift();
      }
    });
  }, [socket]);

  return &lt;div&gt;Streaming {bufferRef.current.length} points&lt;/div&gt;;
}
</code></pre>
<p>This is the "10x faster than <code>NaiveChart</code>, still not great" version. The render doesn't trigger on every push, but you also won't see updates unless something else re-renders. For real plotting, pair the ref with a <code>requestAnimationFrame</code> draw loop (see Step 2).</p>
<h2 id="heading-step-2-separate-shape-from-values">Step 2: Separate Shape from Values</h2>
<p>React renders when the <em>shape</em> of the UI changes. New plot? Re-render. Removed plot? Re-render. Switched from line to bar? Re-render. None of those happen at 1000Hz. They happen a few times a minute, when the user clicks something.</p>
<p>The <em>values</em> inside each plot change at the data rate. Those should never touch React.</p>
<p>Concretely:</p>
<pre><code class="language-tsx">function LivePlot({ store }: { store: SampleStore }) {
  const canvasRef = useRef&lt;HTMLCanvasElement&gt;(null);

  useEffect(() =&gt; {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d")!;

    let raf = 0;
    const draw = () =&gt; {
      const samples = store.read();
      drawSeries(ctx, samples);
      raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () =&gt; cancelAnimationFrame(raf);
  }, [store]);

  return &lt;canvas ref={canvasRef} width={1200} height={300} /&gt;;
}
</code></pre>
<p>React renders this component once. The <code>useEffect</code> runs once. The <code>requestAnimationFrame</code> loop reads from the store on every frame.</p>
<p>The store can be pushed to at any rate the source can manage. The user sees a smooth 60fps line whether the source is sending 100 samples per second or 100,000. The instinct to wire <code>samples</code> into <code>useState</code> is wrong, so resist it.</p>
<h3 id="heading-selective-subscriptions-when-you-do-need-react-in-the-loop">Selective Subscriptions When You Do Need React in the Loop</h3>
<p>Sometimes a component genuinely depends on the data (summary stats, axis labels, or a live value badge). For that, use a store with selective subscriptions so only the components that care re-render. Zustand makes this trivial:</p>
<pre><code class="language-ts">import { create } from "zustand";

const useDataStore = create&lt;{ latest: number | null; setLatest: (v: number) =&gt; void }&gt;((set) =&gt; ({
  latest: null,
  setLatest: (v) =&gt; set({ latest: v }),
}));

function LatestBadge() {
  // Only re-renders when latest changes, not when other store fields do.
  const latest = useDataStore((state) =&gt; state.latest);
  return &lt;div&gt;Latest: {latest?.toFixed(2)}&lt;/div&gt;;
}
</code></pre>
<p>Pair this with RAF coalescing on the writes (call <code>setLatest</code> once per frame, not once per sample) and you get a React component that updates smoothly at 60fps no matter what the data rate is.</p>
<p><code>useSyncExternalStore</code> is the native equivalent and works against any pub-sub store, including the ring buffer above. Use whichever feels lighter for your team.</p>
<h2 id="heading-step-3-move-heavy-work-off-the-main-thread">Step 3: Move Heavy Work Off the Main Thread</h2>
<p>The store and imperative draw loop handle React's contribution to the bottleneck. The main thread itself is still doing all the ingest, parsing, and math. Everything past ~50,000 samples per second per stream needs more.</p>
<p>The browser gives you four escape hatches: Web Workers, transferable objects, <code>SharedArrayBuffer</code> + Atomics, and <code>OffscreenCanvas</code>. Each solves a specific problem.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779369113008/d1d608c9-8078-4339-91e1-c9ee221bf014.png" alt="Browser process model with the renderer process containing the main thread, dedicated workers, shared workers, a service worker, and a compositor thread; the GPU process containing the GPU thread for WebGL and WebGPU; and the network process containing the network thread for fetch and WebSocket, with communication between main and worker types happening via postMessage" style="display: block;" width="1600" height="1006" loading="lazy">

<p>The main thread runs JavaScript, the DOM, layout, and paint setup. Workers are isolated JavaScript contexts with their own event loops. They can't touch the DOM, they can't share memory by default, and every cross-thread message is async and copied unless you transfer it.</p>
<p>Three properties matter as you design against this model:</p>
<ul>
<li><p><strong>Workers can't touch the DOM:</strong> That's the point. They run pure JavaScript. Perfect for data work, network parsing, math, codecs, ML inference.</p>
</li>
<li><p><strong>Communication is async:</strong> No shared variable to read in the middle of a function. Plan APIs around requests and events.</p>
</li>
<li><p><strong>Data copies, unless you transfer or share it:</strong> Structured clone is the default, transferable objects skip the copy, and <code>SharedArrayBuffer</code> skips it permanently.</p>
</li>
</ul>
<h3 id="heading-dedicated-workers-in-practice">Dedicated Workers in Practice</h3>
<p>Here's a minimal worker for ingest:</p>
<pre><code class="language-ts">// worker.ts
self.onmessage = (event) =&gt; {
  const { samples } = event.data;
  // Decode, filter, decimate. The main thread doesn't see any of this.
  const summary = computeStats(samples);
  postMessage(summary);
};
</code></pre>
<pre><code class="language-tsx">// main thread
import { useEffect, useRef } from "react";

export default function WorkerChart({ socket, store }) {
  const workerRef = useRef&lt;Worker&gt;();

  useEffect(() =&gt; {
    workerRef.current = new Worker(new URL("./worker.ts", import.meta.url), {
      type: "module",
    });

    workerRef.current.onmessage = (event) =&gt; {
      store.pushBatch(event.data); // pre-processed, cheap to ingest
    };

    socket.on("newPoint", (point: number) =&gt; {
      workerRef.current?.postMessage({ point });
    });

    return () =&gt; workerRef.current?.terminate();
  }, [socket, store]);

  return &lt;LivePlot store={store} /&gt;;
}
</code></pre>
<p>Three things to internalise about workers.</p>
<p>First, workers are real processes from the runtime's perspective. Each has its own heap, event loop, and <code>globalThis</code>. Starting one costs about 1 to 5ms. Don't spin them up inside hot paths.</p>
<p>Second, module workers are the modern default. <code>type: "module"</code> enables ES modules inside the worker, including <code>import</code>. The legacy <code>importScripts</code> is for classic workers, so avoid it for new code.</p>
<p>Finally, bundlers know about workers. Vite, Webpack, esbuild, and Rspack all detect the <code>new Worker(new URL("./x.ts", import.meta.url))</code> pattern and produce a separate chunk for the worker.</p>
<h3 id="heading-a-worker-pool-that-scales-with-cores">A Worker Pool That Scales with Cores</h3>
<p>For CPU-bound work (parsing binary frames, decoding audio, computing FFTs), a pool spreads jobs across all available cores:</p>
<pre><code class="language-ts">class WorkerPool {
  private workers: Worker[];
  private next = 0;
  private pending = new Map&lt;string, (result: unknown) =&gt; void&gt;();

  constructor(scriptUrl: URL, size = Math.max(1, navigator.hardwareConcurrency - 1)) {
    this.workers = Array.from({ length: size }, () =&gt; {
      const w = new Worker(scriptUrl, { type: "module" });
      w.onmessage = (event) =&gt; {
        const cb = this.pending.get(event.data.id);
        if (!cb) return;
        this.pending.delete(event.data.id);
        cb(event.data.result);
      };
      return w;
    });
  }

  async run&lt;T&gt;(kind: string, payload: unknown, transferables: Transferable[] = []): Promise&lt;T&gt; {
    const id = crypto.randomUUID();
    const result = new Promise&lt;T&gt;((resolve) =&gt; this.pending.set(id, resolve as (r: unknown) =&gt; void));
    const worker = this.workers[this.next];
    this.next = (this.next + 1) % this.workers.length;
    worker.postMessage({ id, kind, payload }, transferables);
    return result;
  }
}

export const pool = new WorkerPool(new URL("./decoder.worker.ts", import.meta.url));
</code></pre>
<p>Round-robin distribution with one worker per core minus one. Pending requests resolve when the matching response arrives. It's cheap, predictable, and scales linearly until you hit memory bandwidth limits.</p>
<h3 id="heading-transferable-objects-the-no-copy-path">Transferable Objects, the No-copy Path</h3>
<p><code>postMessage</code> clones the payload by default. Cloning a 10MB buffer takes milliseconds and doubles your memory use. The fix is to <strong>transfer</strong> the buffer instead.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779369124743/0a53c521-618c-48ec-a5c5-935e1acdbf1a.png" alt="Side-by-side comparison of postMessage modes: default postMessage clones the buffer with a copy landing in both the main heap and the worker heap via structured clone, while Transferable transfers ownership so the same buffer exists once with ownership moving from main to worker" style="display: block;" width="1600" height="4546" loading="lazy">

<p>The semantics: when you transfer a buffer, the sender loses access. The receiver gains it, with no copy and an O(1) handoff.</p>
<pre><code class="language-ts">const buf = new ArrayBuffer(10 * 1024 * 1024); // 10 MB
new Uint8Array(buf).set(somePayload);

worker.postMessage({ buf }, [buf]); // second arg = list of transferables
// `buf` is now detached on this side. Accessing it throws.
</code></pre>
<p>The list of transferable types in 2026 includes <code>ArrayBuffer</code> (and any typed-array view backed by one), <code>MessagePort</code>, <code>ImageBitmap</code>, <code>OffscreenCanvas</code>, the stream types (<code>ReadableStream</code>, <code>WritableStream</code>, <code>TransformStream</code>), <code>RTCDataChannel</code>, <code>VideoFrame</code>, <code>AudioData</code>, and the WebTransport streams. The most useful for React plus real-time work are <code>ArrayBuffer</code>, <code>OffscreenCanvas</code>, and <code>MessagePort</code>.</p>
<p>A common pitfall is that forgetting to transfer creates silent slowness. The app works, but it copies every message. Profile worker <code>postMessage</code> calls. If they're showing milliseconds for "small" payloads, you're cloning when you should be transferring.</p>
<pre><code class="language-ts">// Bad: copies every frame.
worker.postMessage({ samples: float32Array });

// Good: transfers the underlying buffer.
worker.postMessage({ samples: float32Array }, [float32Array.buffer]);
</code></pre>
<p>The buffer is detached after transfer, so the sender needs to re-allocate (or pull from a pool of pre-allocated buffers) if it wants to keep producing.</p>
<h3 id="heading-sharedarraybuffer-and-atomics">SharedArrayBuffer and Atomics</h3>
<p>Transfer hands a buffer off. <strong>Sharing</strong> lets both threads see the same memory simultaneously.</p>
<pre><code class="language-ts">const sab = new SharedArrayBuffer(1024 * 1024); // 1 MB shared
worker.postMessage({ sab });

// Both main and worker now hold references to the same memory.
const viewMain = new Int32Array(sab);
// Inside worker:
// const viewWorker = new Int32Array(event.data.sab);
</code></pre>
<p>Three properties matter.</p>
<p><code>SharedArrayBuffer</code> needs cross-origin isolation. Your page must be served with <code>Cross-Origin-Opener-Policy: same-origin</code> and <code>Cross-Origin-Embedder-Policy: require-corp</code>. Without these headers, <code>SharedArrayBuffer</code> is undefined in the browser. Covered in more detail later in the article.</p>
<p>You also need <code>Atomics</code> for synchronisation. Multiple threads writing to the same memory without coordination produces undefined results. <code>Atomics</code> gives you compare-and-swap, load, store, add, sub, wait, and notify operations on typed array views.</p>
<pre><code class="language-ts">const sab = new SharedArrayBuffer(8);
const view = new Int32Array(sab);

// Main thread: wake any worker that's waiting on slot 0.
Atomics.store(view, 0, 1);
Atomics.notify(view, 0, 1);

// Worker: block until slot 0 changes from 0.
const result = Atomics.wait(view, 0, 0); // "ok", "not-equal", or "timed-out"
</code></pre>
<p>And lock-free ring buffers are the killer app. A producer-consumer queue between two threads with no locking, no <code>postMessage</code> round-trip, and no GC pressure. The data sits in the shared buffer. Atomics coordinate read/write positions.</p>
<p>Here's a minimal SPSC (single-producer, single-consumer) ring buffer:</p>
<pre><code class="language-ts">type SharedRing = {
  data: Float32Array;          // payload
  control: Int32Array;         // [head, tail]
};

function createSharedRing(capacity: number): SharedRing {
  const sab = new SharedArrayBuffer(capacity * 4 + 16);
  const control = new Int32Array(sab, 0, 4);   // [head, tail, ...]
  const data = new Float32Array(sab, 16, capacity);
  return { data, control };
}

function push(ring: SharedRing, value: number): boolean {
  const head = Atomics.load(ring.control, 0);
  const tail = Atomics.load(ring.control, 1);
  const next = (head + 1) % ring.data.length;
  if (next === tail) return false; // full
  ring.data[head] = value;
  Atomics.store(ring.control, 0, next);
  return true;
}

function pop(ring: SharedRing): number | null {
  const tail = Atomics.load(ring.control, 1);
  const head = Atomics.load(ring.control, 0);
  if (tail === head) return null; // empty
  const value = ring.data[tail];
  Atomics.store(ring.control, 1, (tail + 1) % ring.data.length);
  return value;
}
</code></pre>
<p>The producer writes from one thread. The consumer reads from another, neither blocks, and there's no <code>postMessage</code> between them. At signal rates, this is the only architecture that scales.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779369132294/fcefe558-040e-41d3-b759-a8c10c5d5502.png" alt="Single-producer single-consumer ring buffer over SharedArrayBuffer: the producer (ingest worker) writes at the head pointer and increments it, the consumer (render thread) reads from the tail pointer and increments it, both pointers stored in the shared buffer and updated with atomic operations" style="display: block;" width="1600" height="308" loading="lazy">

<p>For multi-producer or multi-consumer queues, you need compare-and-swap loops (<code>Atomics.compareExchange</code>). They get fiddly fast. Most production setups use SPSC where they can and fall back to message-passing where they can't.</p>
<h3 id="heading-in-electron-ingest-in-the-main-process">In Electron, Ingest in the Main Process</h3>
<p>The same idea applies one level up. With Electron and a native SDK, you can ingest samples in the <strong>main process</strong>, buffer there, and forward batches across IPC at the renderer's refresh cadence. One IPC message per sample at 1kHz will saturate IPC. One IPC message per frame with a batch of 16 samples is trivial.</p>
<pre><code class="language-ts">// electron main: buffer in Node, flush at 60Hz
let pending: number[] = [];

device.on("sample", (value) =&gt; pending.push(value));

setInterval(() =&gt; {
  if (pending.length === 0) return;
  const batch = new Float32Array(pending);
  pending = [];
  // Transferable to avoid the structured-clone copy.
  mainWindow.webContents.send("device:samples", batch.buffer, [batch.buffer]);
}, 1000 / 60);
</code></pre>
<pre><code class="language-ts">// preload: expose a thin subscription API to the renderer
contextBridge.exposeInMainWorld("device", {
  onSamples: (cb: (samples: Float32Array) =&gt; void) =&gt; {
    const handler = (_: unknown, buf: ArrayBuffer) =&gt; cb(new Float32Array(buf));
    ipcRenderer.on("device:samples", handler);
    return () =&gt; ipcRenderer.removeListener("device:samples", handler);
  },
});
</code></pre>
<pre><code class="language-tsx">// renderer: feed the store from the bridge
useEffect(() =&gt; {
  return window.device.onSamples((batch) =&gt; store.pushBatch(batch));
}, []);
</code></pre>
<p>The renderer thread of the renderer touches only one batch per frame, no matter how fast the device is. The data work happens upstream.</p>
<h2 id="heading-step-4-render-off-main-with-offscreencanvas">Step 4: Render Off Main with <code>OffscreenCanvas</code></h2>
<p>The DOM is single-threaded. The Canvas API used to be too. <code>OffscreenCanvas</code> breaks that: a canvas you can transfer to a worker, where it draws independently of main.</p>
<pre><code class="language-ts">// Main thread
const canvas = canvasRef.current!;
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
</code></pre>
<pre><code class="language-ts">// Worker
let ctx: OffscreenCanvasRenderingContext2D | null = null;

self.onmessage = (event) =&gt; {
  if (event.data.canvas) {
    ctx = event.data.canvas.getContext("2d");
    return;
  }
  if (event.data.samples &amp;&amp; ctx) {
    drawSeries(ctx, event.data.samples);
  }
};
</code></pre>
<p>The main thread is now free to handle clicks, hover, and other interaction without competing with the draw loop. The worker draws at its own rate, against whatever data it has.</p>
<p>Combined with <code>SharedArrayBuffer</code>, you get the cleanest real-time rendering pipeline available in the browser:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779369135331/1a205236-981d-4df7-8b21-698d15b84a63.png" alt="Rendering pipeline off the main thread: a data source feeds an ingest worker which writes into a SharedArrayBuffer, which a render worker owning an OffscreenCanvas reads to issue GPU commands that update the DOM, while the main thread reads a summary from the SharedArrayBuffer for React UI" style="display: block;" width="1600" height="260" loading="lazy">

<p>The ingest worker writes samples into the shared buffer. The render worker reads them and draws. Main only reads summaries (FPS, latest value, channel labels) and renders chrome. No data ever crosses through main's event loop.</p>
<p>For 19-channel signal at 1kHz, this is the only architecture that keeps a guaranteed 60fps on a laptop, with headroom.</p>
<h2 id="heading-step-5-decimate-before-you-draw">Step 5: Decimate Before You Draw</h2>
<p>A 1200-pixel-wide canvas can show, at best, 1200 distinct X positions. If you have 60,000 samples in your window and you draw all of them, you do 50 times more work than the user can see.</p>
<p>Pick the right point per pixel column. The "min-max" pattern works well for signals: for each pixel column, find the minimum and maximum value in that range and draw a vertical line between them. This is visually identical to drawing every point, but it's much cheaper.</p>
<pre><code class="language-ts">function drawDecimated(
  ctx: CanvasRenderingContext2D,
  samples: Float32Array,
  width: number,
) {
  const samplesPerPixel = samples.length / width;
  ctx.beginPath();
  for (let x = 0; x &lt; width; x++) {
    const start = Math.floor(x * samplesPerPixel);
    const end = Math.floor((x + 1) * samplesPerPixel);
    let min = Infinity;
    let max = -Infinity;
    for (let i = start; i &lt; end; i++) {
      const v = samples[i];
      if (v &lt; min) min = v;
      if (v &gt; max) max = v;
    }
    ctx.moveTo(x, scale(min));
    ctx.lineTo(x, scale(max));
  }
  ctx.stroke();
}
</code></pre>
<p><a href="https://en.wikipedia.org/wiki/Downsampling_(signal_processing)">Decimation</a> is the single biggest CPU win in real-time visualisation, and almost nobody does it.</p>
<p>A more visually faithful variant is <strong>LTTB (Largest Triangle Three Buckets)</strong>, an algorithm that picks one representative sample per bucket while preserving the visual shape better than min-max. It's worth the read if you're plotting non-signal data like stock charts where peaks and dips matter individually. Most good chart libraries (uPlot, Plotly, ECharts) include decimation out of the box.</p>
<h2 id="heading-step-6-wrap-an-external-renderer">Step 6: Wrap an External Renderer</h2>
<p>You don't always need to write the draw loop yourself. React is great at lifecycle management. Imperative chart libraries are great at raw performance. The right move is often to let React mount and unmount the chart while the library handles the fast inner loop.</p>
<pre><code class="language-tsx">import Uplot from "uplot";
import "uplot/dist/uPlot.min.css";
import { useEffect, useRef } from "react";

export default function UPlotChart({ data }: { data: AlignedData }) {
  const ref = useRef&lt;HTMLDivElement&gt;(null);
  const plotRef = useRef&lt;Uplot | null&gt;(null);

  useEffect(() =&gt; {
    const opts = {
      title: "Realtime Chart",
      width: 600,
      height: 300,
      series: [{}, { label: "Signal" }],
    };
    plotRef.current = new Uplot(opts, data, ref.current!);
    return () =&gt; plotRef.current?.destroy();
  }, []);

  useEffect(() =&gt; {
    plotRef.current?.setData(data); // imperative update, no React render
  }, [data]);

  return &lt;div ref={ref} /&gt;;
}
</code></pre>
<p>uPlot, TimeChart, ECharts, Plotly, LightningChart, and the others all follow this shape: instantiate inside <code>useEffect</code>, call <code>setData</code> imperatively, and destroy on unmount. React orchestrates and the library renders.</p>
<p>The general pattern, for any imperative renderer, looks like this:</p>
<pre><code class="language-tsx">function ChartWrapper({ config, data }) {
  const ref = useRef&lt;HTMLDivElement&gt;(null);

  useEffect(() =&gt; {
    const chart = new SomeFastChartLib(ref.current, config);
    chart.setData(data);
    return () =&gt; chart.destroy();
  }, [config, data]);

  return &lt;div ref={ref} /&gt;;
}
</code></pre>
<p>This is the highest-ROI move if you're not in a domain where you need pixel-level control over the drawing. Reach for a library first, and only write your own draw loop when no library fits.</p>
<h2 id="heading-step-7-when-canvas-isnt-enough-reach-for-webgl">Step 7: When Canvas isn't Enough, Reach for WebGL</h2>
<p>Canvas 2D handles a few thousand line segments per frame comfortably. Past that, you start spending milliseconds in <code>stroke()</code> itself.</p>
<p>WebGL (or its higher-level wrappers like <code>regl</code>, <code>twgl</code>, <code>pixi.js</code>, <code>deck.gl</code>) moves the rendering to the GPU. You pay an upfront cost (writing shaders and managing buffers) for the ability to draw millions of points without breaking a sweat.</p>
<p>Here's a minimal WebGL "line strip with a single vertex shader" sketch:</p>
<pre><code class="language-ts">const gl = canvas.getContext("webgl2")!;

const program = gl.createProgram()!;
// ... compile vertex + fragment shaders, link, get attribute location ...

const samplesBuffer = gl.createBuffer();
function drawWebGL(samples: Float32Array) {
  gl.bindBuffer(gl.ARRAY_BUFFER, samplesBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, samples, gl.STREAM_DRAW);
  gl.useProgram(program);
  gl.drawArrays(gl.LINE_STRIP, 0, samples.length);
}
</code></pre>
<p>The pattern is the same as Canvas 2D: an imperative draw call inside the RAF loop. The difference is the GPU does the actual rasterising. Most real-time charting libraries that claim "millions of points" are doing exactly this under the hood.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1779361166770/22241c5c-446b-4026-88fb-33e3caad3831.png" alt="Decision tree comparing rendering options: Canvas 2D for up to a few thousand points, WebGL for tens of thousands or more, and WebGPU for 3D, custom shaders, or compute once stable" style="display: block;" width="1600" height="1351" loading="lazy">

<p>Default to Canvas 2D, and escalate to WebGL only when you can prove Canvas is the bottleneck on the Profiler.</p>
<h2 id="heading-step-8-keep-memory-flat">Step 8: Keep Memory Flat</h2>
<p>Real-time apps die slowly. They run smoothly for an hour and then the laptop fan kicks in. The cause is almost always memory.</p>
<p>There are three rules that keep the heap calm.</p>
<p>First, use typed arrays for numeric data. A <code>Float32Array</code> of 60,000 floats is 240KB. A plain JavaScript array of the same size is around 1.4MB and creates GC pressure on every push.</p>
<p>Second, bound everything. Use ring buffers, not growing arrays. Put caps on history, queues, and pending work. Anything that <em>can</em> grow unbounded eventually will.</p>
<p>Third, reuse buffers. Allocate once in setup, and reuse across frames. Allocating a fresh array per frame at 60fps is 60 allocations per second per plot. Multiply by plot count.</p>
<pre><code class="language-ts">// Bad: fresh array every frame.
function drawFrame() {
  const snapshot = store.read(); // returns a new Float32Array
  drawDecimated(ctx, snapshot, width);
}

// Better: reuse a draw buffer.
const drawBuf = new Float32Array(WINDOW_SIZE);

function drawFrame() {
  store.readInto(drawBuf); // writes into the existing buffer
  drawDecimated(ctx, drawBuf, width);
}
</code></pre>
<p>These rules aren't React-specific. They're the rules any real-time system follows. The reason they need stating is that React-shaped thinking ("derive a new array each render") is the opposite of what real-time wants.</p>
<h2 id="heading-step-9-scheduling-strategies">Step 9: Scheduling Strategies</h2>
<p>A worker pool with no scheduling is just a queue. Sometimes you want priority: a user-driven action should jump ahead of a background sweep.</p>
<h3 id="heading-priority-queues">Priority Queues</h3>
<p>A small priority scheduler:</p>
<pre><code class="language-ts">type Priority = "high" | "normal" | "low";

class PriorityPool {
  private queues: Record&lt;Priority, Job[]&gt; = { high: [], normal: [], low: [] };
  private idle: Worker[];

  enqueue(job: Job, priority: Priority = "normal") {
    if (this.idle.length &gt; 0) {
      const worker = this.idle.pop()!;
      this.dispatch(worker, job);
    } else {
      this.queues[priority].push(job);
    }
  }

  private next(): Job | null {
    for (const p of ["high", "normal", "low"] as const) {
      const j = this.queues[p].shift();
      if (j) return j;
    }
    return null;
  }

  private dispatch(worker: Worker, job: Job) {
    worker.postMessage(job.payload, job.transferables);
    worker.onmessage = (event) =&gt; {
      job.resolve(event.data);
      const next = this.next();
      if (next) this.dispatch(worker, next);
      else this.idle.push(worker);
    };
  }
}
</code></pre>
<p>Three buckets are usually enough: high (user-initiated), normal (steady-state work), low (background sweeps, prefetch, telemetry flushing).</p>
<h3 id="heading-chunkable-work">Chunkable Work</h3>
<p>A job that takes 2 seconds blocks a worker for 2 seconds. If you want to keep workers responsive to higher-priority jobs, the job has to be chunkable.</p>
<pre><code class="language-ts">// In the worker:
self.onmessage = async (event) =&gt; {
  const { id, kind, payload } = event.data;
  if (kind === "decode_large") {
    const total = payload.byteLength;
    for (let i = 0; i &lt; total; i += CHUNK) {
      const chunk = decodeChunk(payload, i, Math.min(i + CHUNK, total));
      self.postMessage({ id, kind: "progress", chunk, offset: i });
      // Yield so the worker can check its message queue.
      await new Promise((r) =&gt; setTimeout(r, 0));
    }
    self.postMessage({ id, kind: "done" });
  }
  if (kind === "cancel") {
    // ... abort the current job
  }
};
</code></pre>
<p>Yielding inside a worker isn't free, but it lets the worker process cancellation messages or higher-priority jobs interleaved with the big task. For long jobs (like a firmware flash, large file decode, or big render), this is essential.</p>
<h3 id="heading-fan-out-and-fan-in">Fan-out and Fan-in</h3>
<p>Split a big job into N pieces, dispatch each to a different worker, and gather the results.</p>
<pre><code class="language-ts">async function decodeFile(buffer: ArrayBuffer): Promise&lt;DecodedFrame[]&gt; {
  const chunks = splitBuffer(buffer, 8);
  const results = await Promise.all(
    chunks.map((chunk) =&gt; pool.run&lt;DecodedFrame[]&gt;("decode", chunk, [chunk])),
  );
  return results.flat();
}
</code></pre>
<p>This gives you linear speedup on parallelisable workloads, up to the worker count. It's critical for batch operations like decoding a recording, summarising a long document, or computing embeddings for a folder.</p>
<h2 id="heading-step-10-measure-sustained-performance">Step 10: Measure Sustained Performance</h2>
<p>A spike on the Profiler is one thing. A slow heap creep over an hour is another. For real-time apps, you measure two things.</p>
<p>First, frame consistency. Are you holding 60fps consistently, or dropping occasional frames? A simple FPS meter:</p>
<pre><code class="language-ts">let lastTime = performance.now();
let frames = 0;

function rafLoop(now: number) {
  frames++;
  if (now - lastTime &gt;= 1000) {
    const fps = (frames * 1000) / (now - lastTime);
    frames = 0;
    lastTime = now;
    console.log(`fps: ${fps.toFixed(1)}`);
  }
  requestAnimationFrame(rafLoop);
}
requestAnimationFrame(rafLoop);
</code></pre>
<p>A more honest measure is the worst frame in a window. Average frame time hides the jank.</p>
<p>Second, heap stability. Open DevTools Memory tab, take a heap snapshot, run the app for ten minutes, take another snapshot, and then compare. The diff should be flat. If it's growing, you have a leak: retained listeners, growing arrays, or closures holding onto large objects.</p>
<p>For real apps, wire heap usage and FPS into your analytics so you spot regressions before users complain.</p>
<h2 id="heading-case-study-19-eeg-channels">Case Study: 19 EEG Channels</h2>
<p>Back to the project that started this article: we have nineteen channels, each at 1kHz, all rendered simultaneously, and all needing to stay smooth across multi-hour recording sessions.</p>
<p>The first build used <strong>LightningChart</strong>. It's powerful, capable, and beautiful. But it's also heavy. Memory usage climbed noticeably, the chart's own internals were doing a lot of work for our use case (which was 19 simple line plots, not full multi-axis financial charts), and the licensing was a friction point.</p>
<p>We switched to <strong>uPlot</strong>. It's tiny, fast, and written specifically for time-series. Memory usage dropped, render time per frame went from "occasionally over budget" to "always under," and my machine stopped sounding like it was about to take off. The chart library change alone bought us most of the headroom we needed.</p>
<p>The architecture around the chart did the rest. The pipeline runs on four threads:</p>
<ul>
<li><p><strong>One ingest worker</strong> reads from the device SDK over IPC (Electron main process to renderer).</p>
</li>
<li><p><strong>One</strong> <code>SharedArrayBuffer</code> holds the rolling window for all 19 channels.</p>
</li>
<li><p><strong>One render worker</strong> reads the SAB and draws to an <code>OffscreenCanvas</code>.</p>
</li>
<li><p><strong>Main thread</strong> renders chrome: channel labels, controls, and the FPS meter.</p>
</li>
</ul>
<p>The shared buffer layout is one big <code>Float32Array</code> indexed as <code>[channel * samplesPerChannel + sampleIndex]</code>. The ingest worker writes new samples and advances per-channel head pointers (stored in an <code>Int32Array</code> slice of the SAB). The render worker reads the latest window each frame.</p>
<pre><code class="language-ts">const CHANNELS = 19;
const WINDOW_SAMPLES = 60_000;

const sab = new SharedArrayBuffer(CHANNELS * WINDOW_SAMPLES * 4 + CHANNELS * 4);
const heads = new Int32Array(sab, 0, CHANNELS);
const samples = new Float32Array(sab, CHANNELS * 4, CHANNELS * WINDOW_SAMPLES);

function writeSample(channel: number, value: number) {
  const head = Atomics.load(heads, channel);
  samples[channel * WINDOW_SAMPLES + head] = value;
  Atomics.store(heads, channel, (head + 1) % WINDOW_SAMPLES);
}
</code></pre>
<p>The renderer reads the channel buffers, decimates per pixel column, and draws:</p>
<pre><code class="language-ts">// render.worker.ts
function drawFrame() {
  const ctx = offscreenCtx;
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  for (let ch = 0; ch &lt; CHANNELS; ch++) {
    const start = ch * WINDOW_SAMPLES;
    drawDecimatedRow(ctx, samples.subarray(start, start + WINDOW_SAMPLES), ch);
  }
  requestAnimationFrame(drawFrame);
}
</code></pre>
<p>On a 2024 M3 MacBook Pro, this holds 60fps with 19 channels at 1kHz, and 144fps if the display supports it. The main thread stays under 1% utilisation. Workers consume the spare cores. The user feels something that used to require a native app.</p>
<p>The lesson, in short: most of the work is choosing the right tool for the inner loop, and getting out of its way.</p>
<h2 id="heading-benchmarks-single-thread-vs-multi-thread">Benchmarks: Single Thread vs Multi-Thread</h2>
<p>Here are numbers from running comparable workloads on a 2024 M3 MacBook Pro. They're indicative, not promissory.</p>
<table>
<thead>
<tr>
<th>Workload</th>
<th>Main thread only</th>
<th>Worker pool</th>
<th>Workers + SAB</th>
<th>Workers + SAB + OffscreenCanvas</th>
</tr>
</thead>
<tbody><tr>
<td>Parse 100MB binary file</td>
<td>4.2s (UI frozen)</td>
<td>1.1s</td>
<td>1.0s</td>
<td>1.0s</td>
</tr>
<tr>
<td>Decode 1,000 frames</td>
<td>920ms</td>
<td>280ms</td>
<td>240ms</td>
<td>240ms</td>
</tr>
<tr>
<td>Render 1M-point chart</td>
<td>24fps</td>
<td>24fps</td>
<td>28fps</td>
<td>60fps</td>
</tr>
<tr>
<td>Telemetry: 4 streams x 1000Hz</td>
<td>22fps</td>
<td>38fps</td>
<td>55fps</td>
<td>60fps</td>
</tr>
<tr>
<td>EEG: 19 channels x 1kHz</td>
<td>12fps</td>
<td>25fps</td>
<td>48fps</td>
<td>60fps (144fps possible)</td>
</tr>
<tr>
<td>Main-thread JS time per frame</td>
<td>22ms</td>
<td>8ms</td>
<td>4ms</td>
<td>&lt; 1ms</td>
</tr>
<tr>
<td>Memory overhead</td>
<td>baseline</td>
<td>+50MB</td>
<td>+20MB</td>
<td>+20MB</td>
</tr>
<tr>
<td>Worker spin-up latency (first call)</td>
<td>0</td>
<td>2-5ms</td>
<td>2-5ms</td>
<td>5-10ms</td>
</tr>
</tbody></table>
<p>The pattern: workers alone help, and workers plus shared memory help more. Workers plus shared memory plus <code>OffscreenCanvas</code> is what gets you to "the main thread is doing nothing and the chart is still smooth."</p>
<p>For chart-heavy apps, the leap from "workers" to "workers plus <code>OffscreenCanvas</code>" is the biggest single architectural improvement available without leaving the browser.</p>
<h2 id="heading-the-coopcoep-catch">The COOP/COEP Catch</h2>
<p><code>SharedArrayBuffer</code> and high-resolution timers were tightened in 2020 after Spectre/Meltdown. To use them, your page must be served with two HTTP headers:</p>
<pre><code class="language-plaintext">Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
</code></pre>
<p>This opts your page into "cross-origin isolation." Within an isolated context, <code>SharedArrayBuffer</code> exists, <code>performance.now()</code> is high-resolution, and various other restricted APIs work.</p>
<p>Outside an isolated context, <code>SharedArrayBuffer</code> is undefined, <code>performance.now()</code> is throttled to about 1ms precision, and Atomics throw.</p>
<p>The cost is significant. <code>require-corp</code> means every cross-origin resource (images from a CDN, embedded YouTube videos, third-party fonts, analytics scripts) must explicitly opt in by setting <code>Cross-Origin-Resource-Policy: cross-origin</code> or <code>Cross-Origin-Embedder-Policy: credentialless</code>. Many third-party services don't, which breaks their embedding.</p>
<p>There are two practical options:</p>
<ul>
<li><p><strong>For an Electron app:</strong> the renderer process can be configured to use these headers easily. Most production Electron apps that want real-time visualisation enable them by default.</p>
</li>
<li><p><strong>For a browser app:</strong> weigh the embeds you'd lose against the performance you'd gain. If your app is the main attraction (Figma, Google Docs), opt in. If you depend on third-party widgets, the cost is real.</p>
</li>
</ul>
<p>For chart-heavy or signal-heavy apps that need SAB, the Electron path is usually cleaner.</p>
<h2 id="heading-production-tradeoffs">Production Tradeoffs</h2>
<p>Here are the five real costs of everything above.</p>
<ul>
<li><p><strong>Code complexity:</strong> A worker-driven app has 2 to 4 times the source files of a single-threaded one (main + workers + shared types). It's worth it for the right scale, but painful for a trivial app.</p>
</li>
<li><p><strong>Debugging:</strong> Stack traces split across threads. Chrome DevTools handles this well in 2026 (each worker has its own debugger panel), but it's still more work than a single-thread bug.</p>
</li>
<li><p><strong>Bundle size:</strong> Each worker is a separate chunk. Tree-shaking inside workers is sometimes worse than in main (less mature). Audit worker bundles separately.</p>
</li>
<li><p><strong>Startup latency:</strong> Spinning up workers at app start adds 50 to 200ms. Pre-warm them during the splash screen, or accept the first-frame delay.</p>
</li>
<li><p><strong>Browser API gaps:</strong> <code>localStorage</code>, <code>document</code>, and most DOM APIs aren't available in workers. Some libraries silently rely on them and break. Test in a worker context before bundling a library you haven't tried there.</p>
</li>
</ul>
<p>There are three trade-offs specific to the imperative rendering pattern:</p>
<ul>
<li><p><strong>Declarative animation of the data:</strong> The chart frame, labels, and controls all stay declarative. The data inside the chart becomes imperative.</p>
</li>
<li><p><strong>Easy snapshot testing of the rendered output:</strong> A canvas has no DOM you can query. Test the data path separately from the draw path. Snapshot the store output, not the pixels.</p>
</li>
<li><p><strong>React's component story for the inner loop:</strong> The draw loop is a closure. Composing draw loops is harder than composing components. Pick your component boundary carefully so each canvas does one thing.</p>
</li>
</ul>
<p>For most apps these costs aren't worth paying. For an app that has to render 1kHz data smoothly, they're the price of admission.</p>
<h2 id="heading-should-you-build-like-this">Should You Build Like This?</h2>
<p>If your data rate is below 30 updates per second per stream, none of this is needed. Naïve <code>setState</code> per batch will work. Profile first, optimise second.</p>
<p>This architecture earns its keep when:</p>
<ul>
<li><p>You have many streams or sensors</p>
</li>
<li><p>Ingestion is sustained, not bursty</p>
</li>
<li><p>The UX promise is smooth motion for hours, not seconds</p>
</li>
<li><p>You'd rather not rewrite the UI in a native language to get there</p>
</li>
</ul>
<p>The boring rule: start with <code>requestAnimationFrame</code> coalescing and external stores. Promote to workers when those aren't enough. Promote to shared memory when worker <code>postMessage</code> is the bottleneck. Promote to <code>OffscreenCanvas</code> when the render loop itself becomes the bottleneck. Each step is a real architectural investment. Take them in order.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>React can handle real-time visualisation if you use it the right way. Instead of pushing React to do everything, use it as the conductor. Let specialised libraries and workers handle the heavy lifting.</p>
<p>These are the three rules that hold up across every high-frequency React app I've built:</p>
<ol>
<li><p><strong>Workers do the work, while main does the UI.</strong> If main is doing math, you've put the math in the wrong place.</p>
</li>
<li><p><strong>Transfer if you can, share if you must.</strong> Both beat cloning. Sharing is more complex than transferring.</p>
</li>
<li><p><strong>Let React orchestrate. Let specialised tools render.</strong> The store owns the data, the draw loop owns the values, and React owns the shape.</p>
</li>
</ol>
<p>Get those right and your React app stops being a one-core system that flinches at high-frequency data. It becomes a real multi-core system that scales with the hardware, ingests without dropping, and renders without stuttering.</p>
<p>The cores are right there. Use them.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a href="https://github.com/leeoniya/uPlot">uPlot</a>: tiny, fast, focused on time-series.</p>
</li>
<li><p><a href="https://github.com/huww98/TimeChart">TimeChart</a>: high-performance real-time chart on WebGL.</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers">Using Web Workers in React</a>: MDN reference.</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer">SharedArrayBuffer on MDN</a>: the shared-memory primitive.</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas">OffscreenCanvas on MDN</a>: rendering off the main thread.</p>
</li>
<li><p><a href="https://web.dev/articles/coop-coep">COOP and COEP explainer</a>: what cross-origin isolation buys you.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Implement Multi-Threading in Node.js With Worker Threads [Full Handbook] ]]>
                </title>
                <description>
                    <![CDATA[ JavaScript is a single-threaded programming language, and Node.js is the runtime environment for JavaScript. This means that JavaScript essentially runs within Node.js, and all operations are handled through a single thread. But when we perform tasks... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-implement-multi-threading-in-nodejs-with-worker-threads-full-handbook/</link>
                <guid isPermaLink="false">68fba9c10656b6400beb0762</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ multithreading ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Worker Thread ]]>
                    </category>
                
                    <category>
                        <![CDATA[ workers ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sumit Saha ]]>
                </dc:creator>
                <pubDate>Fri, 24 Oct 2025 16:30:57 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761323431527/d74eb2ba-edaa-4d19-a041-364e99a705ba.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>JavaScript is a single-threaded programming language, and Node.js is the runtime environment for JavaScript. This means that JavaScript essentially runs within Node.js, and all operations are handled through a single thread.</p>
<p>But when we perform tasks that require heavy processing, Node.js's performance can start to decline. Many people mistakenly think that Node.js isn’t good or that JavaScript is flawed. But there’s actually a solution. JavaScript can also be used effectively with multi-threading.</p>
<p>In this article, we will focus on the backend: specifically, how to implement multi-threading on the server side using Node.js.</p>
<h2 id="heading-heres-what-well-cover"><strong>Here’s What We’ll Cover</strong></h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup-with-expressjs">Project Setup with ExpressJS</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-create-a-new-project-folder">1. Create a New Project Folder</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-initialize-a-nodejs-project">2. Initialize a Node.js Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-install-expressjs">3. Install Express.js</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-optional-install-nodemon-for-development">4. Optional: Install Nodemon for Development</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-create-the-main-server-file">5. Create the Main Server File</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-run-the-project">6. Run the Project</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-problem">Understanding the Problem</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-observing-the-behavior">Observing the Behavior</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-does-this-happen">Why Does This Happen?</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-javascript-execution">Understanding JavaScript Execution</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-libuv-works">How Libuv Works</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-asynchronous-nature-of-nodejs">Asynchronous Nature of Node.js</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-the-cpu-intensive-problem">The CPU-Intensive Problem</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-worker-threads">How to Implement Worker Threads</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-communication-between-threads">Communication Between Threads</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-worker-communication">Setting Up Worker Communication</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-optimize-with-multiple-cores">How to Optimize with Multiple Cores</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-checking-how-many-cores-your-system-has">Checking How Many Cores Your System Has</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-utilizing-multiple-cores-for-faster-execution">Utilizing Multiple Cores for Faster Execution</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-multi-core-optimization">How to Implement Multi-Core Optimization</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-understanding-the-code-line-by-line">Understanding the Code Line by Line</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-thread-planning-and-configuration">Thread Planning and Configuration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-dividing-work-across-multiple-workers">Dividing Work Across Multiple Workers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-handling-complex-tasks">Handling Complex Tasks</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-performance-comparison">Performance Comparison</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-testing-results">Testing Results</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-performance-metrics">Performance Metrics</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-key-takeaways">Key Takeaways</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-the-multi-core-challenge">The Multi-Core Challenge</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-discovering-available-cores">Discovering Available Cores</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-asynchronous-worker-creation">Asynchronous Worker Creation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-multi-threaded-implementation-strategy">Multi-Threaded Implementation Strategy</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-we-learned">What We Learned</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-final-words">Final Words</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-additional-resources">Additional Resources</a></p>
</li>
</ol>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>To follow along and get the most out of this guide, you should have:</p>
<ol>
<li><p>Basic JavaScript (ES6-style) knowledge</p>
</li>
<li><p>Familiarity with Node.js fundamentals</p>
</li>
<li><p>Web-server basics using Express (or similar)</p>
</li>
<li><p>Understanding of blocking vs non-blocking operations in Node.js / JavaScript</p>
</li>
<li><p>Comfort with asynchronous code (Promises / async/await) and event-based handling</p>
</li>
<li><p>Setting up a simple development environment with Node.js</p>
</li>
</ol>
<p>I’ve also created a video to go along with this article. If you’re the type who likes to learn from video as well as text, you can check it out here:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/JTl6tQ4bqYA" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-project-setup-with-expressjs">Project Setup with ExpressJS</h2>
<p>In this section, we will go through a detailed, beginner-friendly setup for a Node.js project using <a target="_blank" href="https://expressjs.com/">Express</a>. This guide explains every step, so even if you are new to Node.js, you can follow along easily.</p>
<h3 id="heading-1-create-a-new-project-folder">1. Create a New Project Folder</h3>
<p>Start by creating a new folder for your project. Open your terminal or command prompt and run:</p>
<pre><code class="lang-powershell">mkdir node<span class="hljs-literal">-worker</span><span class="hljs-literal">-threads</span>
<span class="hljs-built_in">cd</span> node<span class="hljs-literal">-worker</span><span class="hljs-literal">-threads</span>
</code></pre>
<ul>
<li><p><code>mkdir node-worker-threads</code>: This command creates a new folder named <code>node-worker-threads</code>.</p>
</li>
<li><p><code>cd node-worker-threads</code>: Moves you into the newly created folder where all project files will be stored.</p>
</li>
</ul>
<p>Think of this folder as the home for your project.</p>
<h3 id="heading-2-initialize-a-nodejs-project">2. Initialize a Node.js Project</h3>
<p>Every Node.js project needs a <code>package.json</code> file to manage dependencies and scripts. Run:</p>
<pre><code class="lang-powershell">npm init <span class="hljs-literal">-y</span>
</code></pre>
<ul>
<li><p><code>npm init</code> creates a <code>package.json</code> file.</p>
</li>
<li><p>The <code>-y</code> flag automatically fills in default values, saving you time.</p>
</li>
</ul>
<p>After this, you will see a <code>package.json</code> file in your project folder. This file keeps track of all packages and configurations.</p>
<h3 id="heading-3-install-expressjs">3. Install Express.js</h3>
<p>Express is a lightweight web framework for Node.js. Install it with:</p>
<pre><code class="lang-powershell">npm install express
</code></pre>
<p>This adds Express to your project and allows you to create routes, handle requests, and send responses easily.</p>
<h3 id="heading-4-optional-install-nodemon-for-development">4. Optional: Install Nodemon for Development</h3>
<p>Nodemon automatically restarts your server whenever you make changes. This is very useful during development.</p>
<pre><code class="lang-powershell">npm install <span class="hljs-literal">-D</span> nodemon
</code></pre>
<p>The <code>-D</code> flag installs Nodemon as a development dependency.</p>
<p>Next, Update <code>package.json</code> scripts:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"nodemon index.js"</span>
  }
}
</code></pre>
<p>Now you can start the server with:</p>
<pre><code class="lang-powershell">npm run dev
</code></pre>
<p>This will automatically restart your server whenever you make code changes.</p>
<h3 id="heading-5-create-the-main-server-file">5. Create the Main Server File</h3>
<p>Create a file called <code>index.js</code>. This will be the main entry point of your application:</p>
<pre><code class="lang-powershell">touch index.js
</code></pre>
<p>Open <code>index.js</code> and add the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// index.js </span>

<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);

<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> port = process.env.PORT || <span class="hljs-number">3000</span>;

<span class="hljs-comment">// Non-blocking route</span>
app.get(<span class="hljs-string">"/non-blocking"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">"This page is non-blocking."</span>);
});

<span class="hljs-comment">// Blocking route using Worker Threads</span>
app.get(<span class="hljs-string">"/blocking"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> result = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">1000000000</span>; i++) {
    result++;
  }
  res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">`Result is <span class="hljs-subst">${result}</span>`</span>);
});

<span class="hljs-comment">// Start the server</span>
app.listen(port, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`App listening on port <span class="hljs-subst">${port}</span>`</span>);
});
</code></pre>
<p>Here’s what’s going on in this code:</p>
<ul>
<li><p><code>express</code>: To create the server.</p>
</li>
<li><p><code>Worker</code>: To run CPU-intensive tasks in a separate thread.</p>
</li>
<li><p><code>/non-blocking</code> route: Sends a quick response immediately.</p>
</li>
<li><p><code>/blocking</code> route: Runs a Worker thread to handle heavy computation.</p>
</li>
<li><p><code>app.listen</code>: Starts the server on port 3000 (or environment port).</p>
</li>
</ul>
<p>Don’t worry if all of this isn’t perfectly clear at the moment. We’ll explore everything in greater detail as we move forward. Get ready, because we’re going to break down each part step by step in the simplest way possible.</p>
<h3 id="heading-6-run-the-project">6. Run the Project</h3>
<p>Start the server using Nodemon:</p>
<pre><code class="lang-powershell">npm run dev
</code></pre>
<p>Or without Nodemon:</p>
<pre><code class="lang-powershell">node index.js
</code></pre>
<p>Visit these URLs in your browser:</p>
<ul>
<li><p><a target="_blank" href="http://localhost:3000/non-blocking"><code>http://localhost:3000/non-blocking</code></a> displays a simple non-blocking message.</p>
</li>
<li><p><a target="_blank" href="http://localhost:3000/blocking"><code>http://localhost:3000/blocking</code></a> executes a CPU-intensive task using Worker Threads.</p>
</li>
</ul>
<p><strong>Congratulations!</strong> Your Node.js project with Express is fully set up and ready for development.</p>
<h2 id="heading-understanding-the-problem">Understanding the Problem</h2>
<p>We have already set up a basic Express.js application, which is essentially a Node.js app. In this application, we have defined <strong>two routes</strong>:</p>
<ol>
<li><p><code>/non-blocking</code></p>
</li>
<li><p><code>/blocking</code></p>
</li>
</ol>
<p>The <code>/non-blocking</code> route is straightforward: it simply returns a text response saying, "This page is non-blocking."</p>
<p>On the other hand, the <code>/blocking</code> route contains a heavy computation. It runs a loop up to one million numbers, calculates the sum of all these numbers, and then returns the result.</p>
<p>Finally, the application is set to run on port 3000 using <code>app.listen</code>.</p>
<h3 id="heading-observing-the-behavior">Observing the Behavior</h3>
<p>If you open your browser and visit the <a target="_blank" href="http://localhost:3000/non-blocking"><code>http://localhost:3000/non-blocking</code></a> URL, it works perfectly fine and responds immediately.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761079165626/6d6a3c24-8095-4243-83db-8a44865e5af9.png" alt="Non-blocking Browser" class="image--center mx-auto" width="1920" height="1080" loading="lazy"></p>
<p>But if you visit the <a target="_blank" href="http://localhost:3000/blocking"><code>http://localhost:3000/blocking</code></a> URL, the page keeps loading and doesn’t respond right away.</p>
<p>What's even more interesting is that if you try to access <a target="_blank" href="http://localhost:3000/non-blocking"><code>http://localhost:3000/non-blocking</code></a> <strong>while</strong> <code>/blocking</code> is still running, it also becomes unresponsive.</p>
<p>This demonstrates a key concept: while the <code>/blocking</code> route is executing, even the <code>/non-blocking</code> route cannot respond. In other words, the heavy computation in <code>/blocking</code> <strong>blocks the Node.js event loop</strong>, affecting all other routes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761079338040/ecaf458e-d91a-4752-863e-71ac34081949.gif" alt="Blocking Browser" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h3 id="heading-why-does-this-happen">Why Does This Happen?</h3>
<p>The reason lies in how Node.js works. Node.js is essentially a JavaScript runtime, and as we know, JavaScript is a <strong>single-threaded</strong> programming language. Naturally, Node.js also runs on a single thread by default.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761079437967/3330b62c-54c2-41a9-bccc-8f962e71287c.gif" alt="Single Threaded Programming" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<p>So, where does the problem arise? When you execute the <code>/blocking</code> route, all the JavaScript code runs on the <strong>main thread</strong>. During this time, the main thread is completely busy or blocked. As a result, if another user tries to access the <code>/non-blocking</code> route, they won't get any response because the main thread is still occupied with the previous task.</p>
<p>This is why many people mistakenly think that JavaScript is weak because it's single-threaded. But this perception is not entirely accurate. With the right approach and techniques, JavaScript <strong>can also be used in a multi-threaded way</strong>, allowing you to handle heavy computations without blocking other operations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761079547228/0799f826-3715-4664-b94b-e8f2e80afd04.gif" alt="Weak JS" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h2 id="heading-understanding-javascript-execution"><strong>Understanding JavaScript Execution</strong></h2>
<p>Let's think about the main thread where JavaScript primarily runs. You might ask, where exactly does JavaScript execute? JavaScript runs inside the <strong>JavaScript engine</strong>, which is responsible for converting JavaScript code into machine code.</p>
<p>In the case of Node.js, it runs on the <strong>V8 engine</strong>, which is the same engine used in Google Chrome. The V8 engine operates entirely on a single thread, meaning all JavaScript code executes within just one main thread.</p>
<p>Now, you might wonder: are there any threads other than the main thread? The answer is yes. Apart from the main thread, there are additional threads used to handle different types of tasks. The management and implementation of these threads are handled by a special library called <strong>Libuv</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761079628895/02bc21f7-1d49-4952-9c17-3f57cbbe3488.gif" alt="Understanding JavaScript Execution" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h3 id="heading-how-libuv-works">How Libuv Works</h3>
<p>Libuv is designed to work alongside the V8 Engine. While the V8 Engine executes JavaScript code on the main thread, additional threads are used to handle different types of tasks. For example, operations like database queries, network requests, or file read/write tasks are handled by these extra threads, and the Libuv library manages and coordinates them.</p>
<p>Whenever we perform such tasks, they are actually executed on these extra threads outside the main thread. Libuv instructs the V8 Engine on how to handle these tasks efficiently. These tasks are commonly referred to as <strong>Input/Output operations</strong>, or I/O operations for short. In other words, when performing file read/write, database queries, or network requests, these I/O operations are executed on separate threads without blocking the Main Thread.</p>
<p>But if we have tasks like a large for-loop in our earlier example, or any operation that primarily requires <strong>CPU processing</strong>, they do not fall under I/O operations. In such cases, the task must be executed on the main thread, which inevitably blocks it until the task is completed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761079720366/9f01e68c-ad5f-491a-9e68-d91a4ec5f3fb.gif" alt="How libuv works" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h3 id="heading-asynchronous-nature-of-nodejs"><strong>Asynchronous Nature of Node.js</strong></h3>
<p>Consider a scenario where a client sends a request to the main thread, and this request requires a database query to be executed.</p>
<p>When the user sends such a request, the database query is sent to the database, but importantly, it <strong>does not block</strong> the main thread. Instead, Libuv handles the database query on a <strong>separate thread</strong>, keeping the Main Thread free to handle other tasks.</p>
<p>In this situation, if another user sends a request that does <strong>not</strong> involve any database query or I/O operation, it can be executed immediately on the Main Thread. As a result, this second user receives a response without any delay.</p>
<p>Once the database query running on the separate thread completes, the result is returned to the Main Thread, which then sends it back as a response to the original user. This approach ensures that users receive their output efficiently, and the main thread remains available for other tasks.</p>
<p>This entire process represents the <strong>asynchronous nature</strong> of JavaScript and Node.js. Tasks are not executed synchronously – instead, they run asynchronously. One user's request can be processed on a separate thread while other users continue to interact with the server seamlessly. This is how Node.js maintains high performance and responsiveness even under multiple simultaneous requests.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761079806477/dd7b58e2-48ab-4994-ad09-41fad2f0b78b.gif" alt="Asynchronous Nature of Node.js" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h3 id="heading-the-cpu-intensive-problem"><strong>The CPU-Intensive Problem</strong></h3>
<p>So, this is how everything works effectively. Now, the question is, what happens if the main thread has a task that doesn't require any database access for a user's request but demands heavy CPU processing? In that case, the main thread will get blocked.</p>
<p>Let's say a task on the main thread is consuming a lot of CPU. If we execute it directly on the main thread, the event loop will get blocked, and other requests won't be able to be processed.</p>
<p>This is where <strong>worker threads</strong> come into play in Node.js. With worker threads, we can spin up a new thread outside the main thread to handle CPU-heavy operations separately. As a result, the main thread stays free, allowing other requests to be processed immediately.</p>
<p>In other words, by using worker threads, we can run <strong>CPU-bound</strong> tasks <strong>asynchronously</strong>, ensuring that the server's throughput and responsiveness are not affected.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761079890972/98686661-0c5f-493e-a0b4-25c744c60938.gif" alt="CPU Intensive Problem" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h2 id="heading-how-to-implement-worker-threads"><strong>How to Implement Worker Threads</strong></h2>
<p>If we take a look at our previous <code>index.js</code> file, the task in the <code>/blocking</code> route handler is running entirely on the main thread, which is why it causes blocking. So, how can we solve this problem? The solution is to use Node.js's built-in worker threads module.</p>
<p>There is <strong>no need to install any external package</strong>, as worker threads is a core module of Node.js.<br>We can directly require the <code>Worker</code> class from the <code>worker_threads</code> module and create a new worker thread.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// index.js</span>

<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> { Worker } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"worker_threads"</span>);

<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> port = process.env.PORT || <span class="hljs-number">3000</span>;

<span class="hljs-comment">// Non-blocking route</span>
app.get(<span class="hljs-string">"/non-blocking"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">"This page is non-blocking."</span>);
});

<span class="hljs-comment">// Blocking route using Worker Threads</span>
app.get(<span class="hljs-string">"/blocking"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> worker = <span class="hljs-keyword">new</span> Worker(<span class="hljs-string">"./worker.js"</span>);

  <span class="hljs-keyword">let</span> result = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">1000000000</span>; i++) {
    result++;
  }
  res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">`Result is <span class="hljs-subst">${result}</span>`</span>);
});

<span class="hljs-comment">// Start the server</span>
app.listen(port, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`App listening on port <span class="hljs-subst">${port}</span>`</span>);
});
</code></pre>
<p>How it works:</p>
<ul>
<li><p>Inside the <code>/blocking</code> route handler, we create a new worker using <code>new Worker()</code> and provide a file path.</p>
</li>
<li><p>This file (<code>worker.js</code>) contains the <strong>CPU-heavy</strong> task that we want the worker to execute.</p>
</li>
<li><p>For example, our heavy for-loop is moved into this separate file.</p>
</li>
</ul>
<p>We create a new file named <code>worker.js</code> and paste the loop there:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// worker.js</span>

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">1000000000</span>; i++) {
  result++;
}
</code></pre>
<p>When we pass the path to <code>worker.js</code> while creating the Worker, Node.js starts a new thread.</p>
<p>This new thread executes the CPU-intensive task independently, keeping the main thread free to handle other incoming requests.</p>
<p>By doing this, the application becomes more responsive and can handle multiple requests without blocking.</p>
<h3 id="heading-communication-between-threads">Communication Between Threads</h3>
<p>In Node.js, we have the main thread and additional worker threads. To coordinate tasks between them, we can use a <strong>messaging system</strong>. Essentially, all results eventually need to reach the main thread. Otherwise, we won't be able to provide any output to the user.</p>
<p>For example, suppose you assign a task to Thread B and another task to Thread C. When these threads complete their tasks, they must inform the main thread. They do this by sending messages through the messaging system.</p>
<p>Think of it like exchanging messages in an inbox: Thread C sends a message directly to the main thread once its task is finished. Through this communication, worker threads notify the main thread about task completion and send any necessary data.</p>
<p>This is exactly the mechanism we will use in our example to handle CPU-heavy tasks with worker threads, ensuring that the main thread remains free and responsive.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761079966779/34f22f5e-9334-4e89-b54f-71de3de90923.gif" alt="Communication between threads" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h3 id="heading-setting-up-worker-communication">Setting Up Worker Communication</h3>
<p>So, we’ve created a <code>worker.js</code> file. Now, the question is, how do we inform the main thread about the task being done in this file?</p>
<p>To achieve this, we extract <code>parentPort</code> from the built-in <code>worker_threads</code> module in Node.js. The <code>parentPort</code> is a special object that allows communication <strong>between the worker thread and the main thread</strong>. It acts as a bridge: whenever the worker completes a task, it can send the result back to the main thread through this channel.</p>
<p>Once the task is complete, we use the method <code>parentPort.postMessage(result)</code> to send the final data. In other words, we’re posting a message to the parent thread, and in our case, that message is the computed result of our loop.</p>
<p>Here’s the full code for the <code>worker.js</code> file:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// worker.js</span>

<span class="hljs-keyword">const</span> { parentPort } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"worker_threads"</span>);

<span class="hljs-keyword">let</span> result = <span class="hljs-number">0</span>;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10000000000</span>; i++) {
  result++;
}

parentPort.postMessage(result);
</code></pre>
<p>In this example:</p>
<ul>
<li><p>We import parentPort from worker_threads.</p>
</li>
<li><p>We perform a heavy task – a loop that counts up to 10 billion.</p>
</li>
<li><p>After finishing the loop, we send the result back to the main thread using <code>parentPort.postMessage(result)</code>.</p>
</li>
</ul>
<p>This is how communication between the worker thread and the main thread takes place in Node.js.</p>
<p>Now, the question is, once we send the data from the worker, how do we <strong>receive it</strong> in the <code>/blocking</code> handler of our <code>index.js</code> file?</p>
<p>To do this, we need to set up a <strong>listener</strong> inside the handler. For that, we use the <code>worker.on()</code> method.</p>
<p>So, what exactly are we listening for? We listen for the <code>"message"</code> event – just like we listen for <code>onClick</code> or other events in JavaScript.</p>
<p>The first parameter of <code>worker.on()</code> is the event name (<code>"message"</code>), and the second parameter is a <strong>callback function</strong>. Inside that callback, the first argument represents the data we receive from the worker.</p>
<p>Once we receive the data, we can send it back to the browser as a response using:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// index.js </span>

<span class="hljs-comment">// Inside the `/blocking` route handler, we listen for messages from the worker thread.</span>
<span class="hljs-comment">// Whenever the worker completes its task and sends a message, </span>
<span class="hljs-comment">// the callback receives the data as the `data` parameter.</span>
<span class="hljs-comment">// We then send this data back to the client as an HTTP response with status code 200.</span>

worker.on(<span class="hljs-string">"message"</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
  res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">`Result is <span class="hljs-subst">${data}</span>`</span>);
});
</code></pre>
<p>Explanation<strong>:</strong></p>
<ul>
<li><p><code>worker.on("message", callback)</code> listens for messages sent from the worker thread using <code>parentPort.postMessage()</code>.</p>
</li>
<li><p>The <code>data</code> parameter contains the result sent by the worker.</p>
</li>
<li><p>Using <code>res.status(200).send(...)</code>, we send the computed result back to the browser.</p>
</li>
<li><p>This allows the heavy computation to happen in a separate thread, keeping the main thread free and responsive.</p>
</li>
</ul>
<p>At the same time, we should also handle possible errors.</p>
<p>If any error occurs inside the worker, we can listen for it using the <strong>"error"</strong> event in the same way:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// index.js </span>

<span class="hljs-comment">// In the `/blocking` route handler, we listen for any errors that occur inside the worker thread.</span>
<span class="hljs-comment">// If an error occurs, the callback receives the error object `err`,</span>
<span class="hljs-comment">// and we send it back as an HTTP response with status code 400.</span>

worker.on(<span class="hljs-string">"error"</span>, <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
  res.status(<span class="hljs-number">400</span>).send(<span class="hljs-string">`An Error occurred : <span class="hljs-subst">${err}</span>`</span>);
});
</code></pre>
<p>Explanation<strong>:</strong></p>
<ul>
<li><p><code>worker.on("error", callback)</code> listens specifically for errors inside the worker thread.</p>
</li>
<li><p>The <code>err</code> parameter contains details about what went wrong in the worker.</p>
</li>
<li><p>Using <code>res.status(400).send(...)</code>, we return the error to the client so the request doesn’t hang silently.</p>
</li>
</ul>
<p><strong>Here’s how the complete code looks:</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// index.js</span>

<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> { Worker } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"worker_threads"</span>);

<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> port = process.env.PORT || <span class="hljs-number">3000</span>;

<span class="hljs-comment">// Non-blocking route</span>
app.get(<span class="hljs-string">"/non-blocking"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">"This page is non-blocking."</span>);
});

<span class="hljs-comment">// Blocking route using worker threads</span>
app.get(<span class="hljs-string">"/blocking"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> worker = <span class="hljs-keyword">new</span> Worker(<span class="hljs-string">"./worker.js"</span>);

  worker.on(<span class="hljs-string">"message"</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
    res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">`Result is <span class="hljs-subst">${data}</span>`</span>);
  });

  worker.on(<span class="hljs-string">"error"</span>, <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
    res.status(<span class="hljs-number">400</span>).send(<span class="hljs-string">`An Error occured : <span class="hljs-subst">${err}</span>`</span>);
  });
});

<span class="hljs-comment">// Start the server</span>
app.listen(port, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`App listening on port <span class="hljs-subst">${port}</span>`</span>);
});
</code></pre>
<p>Once this is set up, you'll see a dramatic change. The <code>/blocking</code> route is loading, but even while it's loading, repeatedly refreshing the <code>/non-blocking</code> route works perfectly without any issues!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761080061445/f1f213c7-6cce-4334-81e5-8cd828682f8e.gif" alt="Setting up worker communication" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<p>Now notice, the <code>/non-blocking</code> route is accessible, which means even though the <code>/blocking</code> route is still running, it doesn't affect anything. So, we've successfully solved this problem. We moved the main task to a separate thread outside the main thread. What does this mean? The main thread created a new worker thread and assigned the CPU-heavy task to it. The new thread now works independently, while the main thread remains free.</p>
<p>Finally, when the new thread completes its task, it also becomes free. Then, through the messaging system, the new thread informs the main thread, "Your data is ready, here's your data." The main thread receives this data and sends it to the client as a response.</p>
<p>Therefore, the tasks that were automatically handled on separate threads for database queries or file read-write operations – because they were I/O operations – we have now manually initiated a thread and used it to handle similar CPU-heavy tasks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761080131679/148cc279-e4f0-4f68-b2a9-34110abcbc90.gif" alt="IO Operations" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h2 id="heading-how-to-optimize-with-multiple-cores">How to Optimize with Multiple Cores</h2>
<p>Now that you have a clear understanding of how the process works, let's take it one step further and optimize it using multiple CPU cores.</p>
<p>When you visit the <code>/blocking</code> route, you might notice that it still takes a significant amount of time to respond. This indicates that the optimization isn't fully complete yet. So far, we've used a separate thread meaning we've utilized <strong>one CPU core</strong> outside the main thread. But most modern machines have <strong>multiple cores</strong>, and we can take advantage of that to improve performance.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761080243361/dddd924c-9138-4790-ac6c-811b39772c6c.gif" alt="Final index" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h3 id="heading-checking-how-many-cores-your-system-has">Checking How Many Cores Your System Has</h3>
<p>Before assigning multiple cores, you can check how many cores are available on your system:</p>
<ul>
<li><p><strong>macOS (Unix-based):</strong></p>
<pre><code class="lang-powershell">  sysctl <span class="hljs-literal">-n</span> hw.ncpu
</code></pre>
<p>  This command returns the total number of CPU cores on your machine. For example, on my Mac, it shows <code>10</code>, meaning I have ten cores available.</p>
</li>
<li><p><strong>Linux:</strong></p>
<pre><code class="lang-powershell">  nproc
</code></pre>
<p>  This will print the number of processing units available.</p>
</li>
<li><p><strong>Windows (Command Prompt):</strong></p>
<pre><code class="lang-powershell">  <span class="hljs-built_in">echo</span> %NUMBER_OF_PROCESSORS%
</code></pre>
</li>
</ul>
<p>Each of these commands will help you determine how many cores you can use for parallel processing.</p>
<h3 id="heading-utilizing-multiple-cores-for-faster-execution">Utilizing Multiple Cores for Faster Execution</h3>
<p>Once you know how many cores your machine has, you can decide how many of them to allocate for a specific job. For example, since my system has ten cores, I might choose to use four cores for the task.</p>
<p>By distributing the workload across multiple threads (each running on its own core), you can achieve significant performance improvements. Instead of relying on just one core, the system can execute multiple parts of the task simultaneously reducing the total execution time dramatically.</p>
<p>In short, the more cores you effectively utilize, the faster your computationally heavy tasks can complete (as long as your code is designed to handle parallel execution safely).</p>
<h2 id="heading-how-to-implement-multi-core-optimization">How to Implement Multi-Core Optimization</h2>
<p>Now, we'll optimize the <code>/blocking</code> task by using multiple worker threads. First, we’ll create copies of our existing files:</p>
<ul>
<li><p><code>index.js</code> → <code>index-optimized.js</code></p>
</li>
<li><p><code>worker.js</code> → <code>worker-optimized.js</code></p>
</li>
</ul>
<p>We plan to use four threads. Even though the machine may have more cores, using all could overload the system, so we’ll limit it to four.</p>
<p><strong>index-optimize.js:</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// index-optimize.js</span>

<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> { Worker } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"worker_threads"</span>);

<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> port = process.env.PORT || <span class="hljs-number">3000</span>;
<span class="hljs-keyword">const</span> THREAD_COUNT = <span class="hljs-number">4</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createWorker</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> worker = <span class="hljs-keyword">new</span> Worker(<span class="hljs-string">"./worker-optimized.js"</span>, {
            <span class="hljs-attr">workerData</span>: {
                <span class="hljs-attr">thread_count</span>: THREAD_COUNT,
            },
        });

        worker.on(<span class="hljs-string">"message"</span>, <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
            resolve(data);
        });

        worker.on(<span class="hljs-string">"error"</span>, <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            reject(<span class="hljs-string">`An Error occured : <span class="hljs-subst">${err}</span>`</span>);
        });
    });
}

app.get(<span class="hljs-string">"/non-blocking"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">"This page is non-blocking."</span>);
});

app.get(<span class="hljs-string">"/blocking"</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
    <span class="hljs-keyword">const</span> workerPromise = [];

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; THREAD_COUNT; i++) {
        workerPromise.push(createWorker());
    }

    <span class="hljs-keyword">const</span> threadResults = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(workerPromise);
    <span class="hljs-keyword">const</span> total =
        threadResults[<span class="hljs-number">0</span>] +
        threadResults[<span class="hljs-number">1</span>] +
        threadResults[<span class="hljs-number">2</span>] +
        threadResults[<span class="hljs-number">3</span>];

    res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">`Result is <span class="hljs-subst">${total}</span>`</span>);
});

app.listen(port, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`App listening on port <span class="hljs-subst">${port}</span>`</span>);
});
</code></pre>
<p>Here, we create a <code>createWorker</code> function that returns a Promise. Inside it, the worker is created, and the message and error events are handled. In the <code>/blocking</code> route, we create multiple workers asynchronously, wait for all of them to finish using <code>Promise.all</code>, and then sum the results.</p>
<p><strong>worker-optimize.js:</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// worker-optimize.js</span>

<span class="hljs-keyword">const</span> { parentPort, workerData } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"worker_threads"</span>);

<span class="hljs-keyword">let</span> result = <span class="hljs-number">0</span>;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10000000000</span> / workerData.thread_count; i++) {
    result++;
}

parentPort.postMessage(result);
</code></pre>
<p>Each worker receives <code>thread_count</code> from the main thread and calculates its part of the task. Once done, it sends the result back using <code>parentPort.postMessage</code>. This way, heavy computation is distributed, and the main thread remains free.</p>
<h3 id="heading-understanding-the-code-line-by-line">Understanding the Code Line by Line</h3>
<p>Alright, some of these concepts might seem a bit complex at first. But don't worry! We we’ll go through all the code line by line, explaining everything in detail so that you understand exactly what is happening and why.</p>
<h3 id="heading-thread-planning-and-configuration">Thread Planning and Configuration</h3>
<p>Now, coming to the main point we'll be using threads, right? We've planned to use multiple threads. Let's say we've decided to use four threads. Our machine has ten cores, but we won't use them all because that would consume all our system resources. So, we'll use four threads from four of the available cores.</p>
<p>For this reason, in the <code>index-optimized.js</code> file, we've created a constant to store the number of threads we'll use. Let's say we've set it to 4 here, so that later another developer can easily change it if needed.</p>
<h4 id="heading-the-createworker-function">The createWorker Function</h4>
<p>Then, we've created a new function called <code>createWorker</code>. The purpose of this function is to create a new Worker. Here, we’re returning a promise because the process of creating a Worker is performed asynchronously.</p>
<p>This is because when we create four workers, we want the creation process itself to happen asynchronously, so the main thread doesn't get blocked. After all, creating a worker is essentially a separate process.</p>
<p>The best practice is to create workers asynchronously. That's why we created the <code>createWorker</code> function, which returns a promise. As we know, events are listened to inside a promise, where resolve and reject are used. In the <code>/blocking</code> handler, we can handle the worker's result or any errors through this promise.</p>
<h4 id="heading-creating-a-worker">Creating a Worker</h4>
<p>To create a worker, we use:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> worker = <span class="hljs-keyword">new</span> Worker(<span class="hljs-string">"./worker-optimized.js"</span>);
</code></pre>
<p>Here, we need to provide the path to the Worker file. Then, as the second parameter, we can pass some options. For example, if we want to send some data to the Worker, we use <code>{ workerData }</code>. Inside this <code>workerData</code>, we'll send the <code>THREAD_COUNT</code>, which is stored in our file as <code>THREAD_COUNT</code>.</p>
<p>For instance, we can pass an object in <code>workerData</code> like:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-attr">threadCount</span>: THREAD_COUNT;
}
</code></pre>
<p>When this Worker is being created, we send some properties from <code>index-optimized.js</code> as <code>workerData</code>. This is because in <code>worker-optimized.js</code>, the worker can use <code>parentPort</code> to know how many threads it should use. So, we've included a <code>threadCount</code> property in <code>workerData</code>. When the worker starts, it reads <code>threadCount</code> from <code>workerData</code> and works accordingly. This is how we've designed the <code>createWorker</code> function, which simply returns a Promise.</p>
<h4 id="heading-event-handling-and-promise-structure">Event Handling and Promise Structure</h4>
<p>Here, we made an important change compared to our original <code>index.js</code> file.</p>
<p>Since we copied all the code from <code>index.js</code> into <code>index-optimized.js</code>, we adjusted the <code>/blocking</code> route handler. Specifically, we removed the direct creation of the Worker from the <code>/blocking</code> handler. Instead, the Worker is now created inside the <code>createWorker</code> function.</p>
<p>Also, all the event listeners (<code>message</code> and <code>error</code>) that were previously inside the <code>/blocking</code> handler have also been moved into the <code>createWorker</code> function. This means that the worker is fully managed within the function, and the <code>/blocking</code> handler now only handles the promise results, keeping the main thread clean and organized.</p>
<p>But since these events are being listened to inside a promise, we cannot send the response directly from there. We'll send the response inside the <code>/blocking</code> handler. So from the Promise, we only use <code>resolve</code> and <code>reject</code>.</p>
<p><strong>For example:</strong></p>
<pre><code class="lang-javascript">resolve(<span class="hljs-string">`Result is <span class="hljs-subst">${data}</span>`</span>);
reject(<span class="hljs-string">`An error occurred <span class="hljs-subst">${err}</span>`</span>);
</code></pre>
<p>In other words, the entire process of creating a worker has been moved into the <code>createWorker</code> function, which ultimately returns a promise.</p>
<h3 id="heading-dividing-work-across-multiple-workers">Dividing Work Across Multiple Workers</h3>
<p>Now, inside the <code>/blocking</code> handler, I simply call the <code>createWorker</code> function. The workerData we provide tells the worker what task it should perform. The created worker is linked with parentPort in the <code>worker-optimized.js</code> file, which essentially communicates with the parent thread.</p>
<p>Now, we want to divide the for-loop running up to one million across four cores. The number of cores to use is sent from <code>index-optimized.js</code> as part of workerData. Because this information is in workerData, the workers can automatically divide and handle the tasks among themselves.</p>
<p>So, in the <code>worker-optimized.js</code> file, we'll get the workerData using:</p>
<pre><code class="lang-javascript">{ workerData } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"worker_threads"</span>)
</code></pre>
<p>Then, in the for-loop condition, we'll use <code>workerData.threadCount</code>. This means the threadCount sent from <code>index-optimized.js</code> will be used here instead of hardcoding 4. This is best practice because the data is passed to the worker at the time of its creation. In <code>worker-optimized.js</code>, we use this to divide the work into four parts. Then, four workers will be created, meaning the <code>createWorker</code> function will be called four times. Each worker will take one part of the work, and at the end, all results will be combined. This is how the entire process is completed.</p>
<p>So, in this <code>/blocking</code> handler, our task is to collect the results of the four promises and then sum them all. Let's say we store them in an array called <code>workerPromises</code>. Each entry in this array will hold the promise result of a worker. Then, by combining all of them, we get the final result.</p>
<p>Since we need to create four Workers, we'll run a for-loop: <code>for (let i = 0; i &lt; THREAD_COUNT; i++)</code>. Inside the body of this loop, we'll call the <code>createWorker</code> function each time. This means that in every iteration, a new worker is created, and its promise is pushed into the <code>workerPromises</code> array.</p>
<p>So, inside the body of this loop, we'll call the <code>createWorker</code> function four times. Each call to <code>createWorker</code> returns a promise. These four promises are pushed into the <code>workerPromises</code> array, like <code>workerPromises.push(createWorker())</code>. This way, each worker has its own promise. In the end, since all the promises are stored in the <code>workerPromises</code> array, we can easily call <code>Promise.all(workerPromises)</code>.</p>
<p>So, we used <code>threadResults = await Promise.all(workerPromises)</code>. As we know, <code>Promise.all</code> can handle multiple Promises together. Here, we passed the <code>workerPromises</code> array, so <code>threadResults</code> will contain the results of the four promises as separate elements, like <code>threadResults[0]</code>, <code>threadResults[1]</code>, <code>threadResults[2]</code>, and <code>threadResults[3]</code>. Then, we sum these results to get the total calculation, meaning <code>threadResults[0] + threadResults[1] + threadResults[2] + threadResults[3]</code> gives the final result. Since we used await, the entire function needs to be async.</p>
<p>Once everything is done correctly, we can send this total result to the client using <code>res.status(200).send(Result is ${total})</code>. This way, the total calculation works correctly, unlike before.</p>
<p>So, I hope it's clear now: we called the <code>createWorker</code> function four times here. Each call returns a promise. We then awaited all these promises together using <code>Promise.all</code>, so all the results came in at once. After that, we summed these results. The <code>/blocking</code> handler is essentially the one executing our operational work.</p>
<h3 id="heading-handling-complex-tasks">Handling Complex Tasks</h3>
<p>So, in the <code>worker-optimized.js</code> file, we've essentially divided the work into four parts. But it's not necessary that the task will always be a for-loop. There could be different types of complex tasks as well, like image processing, data processing, or pagination.</p>
<p>In such cases, we can't always follow the same pattern. So, we need to send the necessary data from <code>index-optimized.js</code> as <code>workerData</code>, and the worker will use that data to perform the task in a separate process.</p>
<p>In the previous example, all the steps were sequential, so simply summing the results gave us the total. But in the case of complex tasks, we need to use data-driven processing.</p>
<p>In other complex applications, you might need to perform different tasks. But the main concept is clear: any data or property we send from here will be received by the worker, which will then divide the work. Each worker – whether you use four, five, or six – will handle its part, and all the results will need to be accumulated. This is essentially the entire process.</p>
<h2 id="heading-performance-comparison"><strong>Performance Comparison</strong></h2>
<p>When working with CPU-intensive tasks in Node.js, dividing the work using worker threads can significantly improve performance. Let's compare the behavior of our application before and after optimization.</p>
<h3 id="heading-testing-results">Testing Results</h3>
<p>Running the <code>index.js</code> file and hitting the <code>/blocking</code> route in the browser takes a significant amount of time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761164273474/02892dd3-3524-4e7e-83fa-ac279910d759.gif" alt="Final Index" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<p>Running the <code>index-optimized.js</code> file and hitting the same route takes considerably less time – around 3 seconds.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761164303764/4ba27180-49f7-4485-a1a5-73f2f419ab9b.gif" alt="Final Optimized" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<p>Stopping it and running <code>index.js</code> again clearly shows the original implementation is slower.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761164358090/4c3b9056-b936-4341-9302-461c290ea70e.gif" alt="Final unoptimized" class="image--center mx-auto" width="1138" height="640" loading="lazy"></p>
<h3 id="heading-performance-metrics">Performance Metrics</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>File</strong></td><td><strong>Route</strong></td><td><strong>Approx. Response Time</strong></td><td><strong>Notes</strong></td></tr>
</thead>
<tbody>
<tr>
<td><code>index.js</code></td><td><code>/blocking</code></td><td>Much longer</td><td>This is the original implementation. The single-threaded loop blocks the event loop, causing delays.</td></tr>
<tr>
<td><code>index-optimized.js</code></td><td><code>/blocking</code></td><td>Around 3 seconds</td><td>Here, the work is divided into multiple worker threads, making the process much faster.</td></tr>
</tbody>
</table>
</div><h3 id="heading-key-takeaways">Key Takeaways</h3>
<p>This comparison demonstrates how dividing the work into multiple parts using worker threads can make CPU-intensive tasks far more efficient, keeping the main thread responsive and improving overall performance.</p>
<h2 id="heading-summary">Summary</h2>
<p>So, first we saw in <code>index.js</code> how a blocking task can be handled in a <code>non-blocking</code>, asynchronous way. That is, we ran a worker thread, and because of this worker thread, the main thread didn't get blocked, allowing other users to continue their tasks simultaneously.</p>
<h3 id="heading-the-multi-core-challenge">The Multi-Core Challenge</h3>
<p>But the problem is, when we use a new thread on the server, there isn't just a single core. Usually, there are multiple cores, like <code>8</code>, <code>16</code>, or more. To use multiple cores, we first need to find out how many cores are available on the server.</p>
<h3 id="heading-discovering-available-cores">Discovering Available Cores</h3>
<p>If the server is Linux, we can easily find out the total number of cores using the <code>nproc</code> command. Then we can decide how many cores to use. For example, let's say we decide to use three cores. In <code>index-optimized.js</code>, we've implemented a way to divide the work among these cores.</p>
<h3 id="heading-asynchronous-worker-creation">Asynchronous Worker Creation</h3>
<p>So, what we did was wrap the worker creation process in a promise. Since creating a worker takes some time and spinning it up isn't instantaneous, this process is done asynchronously. This way, even if multiple users hit the endpoint to create Workers, the main thread won't be blocked.</p>
<h3 id="heading-how-to-implement-multi-core-optimization-1">How to Implement Multi-Core Optimization</h3>
<p>We simply created workers, and then using the <code>createWorker</code> function inside a loop, we spawned four or a specified number of Workers based on the thread count. Each worker posts messages independently, and through the listener, we receive data from each worker. These results are collected via promises, stored together in an array, and finally, we sum all the results from this array to get the final outcome.</p>
<p>So, the other concepts are all part of basic JavaScript. I hope you now understand how worker threads work and how we can use multi-threaded processes in Node.js. It's an excellent concept and a great opportunity to learn thoroughly.</p>
<h3 id="heading-what-we-learned"><strong>What We Learned</strong></h3>
<p>Worker Threads in Node.js provide a powerful way to handle CPU-intensive tasks without blocking the main event loop. By leveraging multiple cores and distributing work across threads, we can significantly improve application performance while maintaining responsiveness for other users.</p>
<ul>
<li><p><strong>Non-blocking execution</strong>: Worker threads prevent the main thread from being blocked</p>
</li>
<li><p><strong>Multi-core utilization</strong>: We can leverage multiple CPU cores for parallel processing</p>
</li>
<li><p><strong>Asynchronous worker creation</strong>: Using promises to handle worker creation without blocking</p>
</li>
<li><p><strong>Result aggregation</strong>: Collecting and combining results from multiple workers</p>
</li>
<li><p><strong>Performance optimization</strong>: Distributing heavy computations across multiple threads</p>
</li>
</ul>
<p>This approach is particularly valuable for applications that need to handle computationally intensive tasks while remaining responsive to user requests.</p>
<h2 id="heading-final-words">Final Words</h2>
<p>If you found the information here valuable, feel free to share it with others who might benefit from it. I’d really appreciate your thoughts – mention me on X <a target="_blank" href="https://x.com/sumit_analyzen">@sumit_analyzen</a> or on Facebook <a target="_blank" href="https://facebook.com/sumit.analyzen">@sumit.analyzen</a>, <a target="_blank" href="https://youtube.com/@logicBaseLabs">watch my coding tutorials</a>, <a target="_blank" href="https://sumitsaha.me">visit my website</a> or simply <a target="_blank" href="https://www.linkedin.com/in/sumitanalyzen/">connect with me</a> on LinkedIn.</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<p>You can also check the <a target="_blank" href="https://nodejs.org/api/worker_threads.html">Node.js Worker Threads documentation</a> for more in-depth learning. You can find all the source code from this tutorial in <a target="_blank" href="https://github.com/logicbaselabs/node-worker-threads/">this GitHub repository</a>. If it helped you in any way, consider giving it a star to show your support!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
