সরাসরি পর্বে যান
পর্ব ৪২ · ৫৯-এর মধ্যে · মডিউল ৬

Web Worker — ভারী কাজ আলাদা thread-এ, পাতা থাকে সচল

Web Workers — Keeping the Main Thread Free: postMessage, Transfer, RPC, Pools & worker_threads

This Bangla lesson moves heavy work off the main thread: why pages freeze, creating a worker with postMessage and onmessage, structured clone costs versus transferable objects, what a worker can and cannot access and how errors surface, a small promise-based RPC layer, real use cases and when not to use workers, plus SharedArrayBuffer, OffscreenCanvas and Node worker_threads.

এই পর্বে যা শিখবেন

  • Main thread কেন জমে যায়, আর Worker কীভাবে সত্যিকারের parallelism দেয়
  • new Worker(), postMessage, onmessage, error ধরা আর terminate()
  • Structured clone-এর খরচ, আর transferable object দিয়ে কপি ছাড়া বড় ডেটা পাঠানো
  • id আর Promise দিয়ে Worker-কে function-এর মতো ডাকা (RPC), worker pool, আর কখন Worker দরকার নেই
  • SharedArrayBuffer, OffscreenCanvas, Node.js-এর worker_threads — আর service worker ও worklet থেকে পার্থক্য

১ · Main thread-এর ঘাড়ে সব কাজ — কেন পাতা জমে যায়

পর্ব ৩১-এ দেখেছেন, ব্রাউজারে আপনার JavaScript চলে main thread-এ। কিন্তু এই thread শুধু আপনার কোডের নয়। ক্লিক আর কী-চাপ সামলানো, style হিসাব, layout, পর্দা আঁকা — সব এখানেই হয়। ফলে কোনো একটি callback যদি দুই সেকেন্ড ধরে হিসাব করে, এই দুই সেকেন্ড ব্রাউজার আর কিছুই করতে পারে না। ঘুরতে থাকা লোডিং চিহ্ন (spinner) থেমে যায়, বোতাম চাপলে সাড়া আসে না। ৫০ ms-এর বেশি এমন কাজকে বলে long task, আর এতে পাতার INP খারাপ হয়।

পর্ব ৩১-এ একটি সমাধান দেখেছেন: কাজ টুকরো করে মাঝে মাঝে main thread ছেড়ে দেওয়া, যাকে বলে chunking. কিন্তু হিসাব যদি সত্যিই বিশাল হয় — দশ লাখ অর্ডারের বিক্রির রিপোর্ট, একটি ১২ মেগাপিক্সেল ছবিতে ফিল্টার, বড় ফাইলের hash — তখন টুকরো করলেও মোট কাজ একই thread-এই হয়। দরকার আরেকটি thread. সেটিই Web Worker.

একটি ব্যাংকের শাখায় কাউন্টারের কর্মকর্তা ভাবুন। গ্রাহক এলে টাকা জমা নেন, প্রশ্নের উত্তর দেন। এখন তাঁকে যদি মাসের পুরো হিসাব মেলাতেও বসতে হয়, লাইন থেমে যায় — সবাই দাঁড়িয়ে থাকেন। ভালো ব্যবস্থা হলো পেছনের অফিসে (back office) আলাদা একজন হিসাবরক্ষক। কাউন্টার থেকে একটি চিরকুট যায়: “মাসের রিপোর্ট বানান”। হিসাবরক্ষক নিজের টেবিলে কাজ করেন, কাউন্টার গ্রাহক সামলাতে থাকে। কাজ শেষে আরেকটি চিরকুট ফেরত আসে। দুজন একই খাতায় একসাথে লেখেন না — শুধু চিরকুট আদান-প্রদান। কাউন্টার = main thread, পেছনের অফিস = Worker, চিরকুট = বার্তা (message).

Worker একটি সত্যিকারের আলাদা thread — অপারেটিং সিস্টেম তাকে আরেকটি CPU core-এ চালাতে পারে। তাই এটি concurrency নয়, সত্যিকারের parallelism: একই মুহূর্তে main thread পাতা আঁকছে, আর Worker হিসাব করছে। প্রতিটি Worker-এর আছে নিজের event loop, নিজের heap, নিজের global object. সে পাতার DOM ছুঁতে পারে না, main thread-এর variable-ও দেখতে পায় না। যোগাযোগের একটিই পথ: বার্তা পাঠানো।

নিচের ডায়াগ্রামে একই ঘটনা দুইভাবে দেখুন — একটি অনলাইন দোকানের অ্যাডমিন পাতায় “বিক্রির রিপোর্ট বানান” বোতাম, যার হিসাবে ২ সেকেন্ড লাগে। প্রথম তিন ধাপ Worker ছাড়া, পরের তিন ধাপ Worker দিয়ে।

সময় অনুযায়ী দুই thread-এর অবস্থা
পদ্ধতি
সময়
Main thread
Worker thread
Task queue (main)
পর্দায়
  1. Worker ছাড়া — ক্লিক এল। বোতামের callback main thread-এই দশ লাখ অর্ডার যোগ করতে শুরু করল। spinner আঁকার সুযোগই পায় না, কারণ আঁকা হয় callback শেষ হলে।
  2. মাঝপথে আরেকটি ক্লিক। ব্যবহারকারী “আজকের অর্ডার” ফিল্টার চাপলেন। ক্লিকটি task queue-তে দাঁড়াল — main thread ব্যস্ত। পর্দা পুরো জমে আছে; এটাই long task, আর এই অপেক্ষাই INP-তে যোগ হয়।
  3. ২ সেকেন্ড পরে সব একসাথে। হিসাব শেষে ফল বসল, তারপর জমে থাকা ক্লিক চলল। ব্যবহারকারীর চোখে অ্যাপটি “হ্যাং করে আবার চালু হলো”।
  4. Worker দিয়ে — ক্লিক এল। এবার callback শুধু Worker-কে একটি বার্তা পাঠাল (postMessage) আর শেষ হয়ে গেল — এক মিলিসেকেন্ডেরও কম। হিসাব শুরু হলো আরেকটি thread-এ। spinner ঘুরতেই থাকল।
  5. মাঝপথের ক্লিক এবার সাথে সাথে। Main thread মুক্ত, তাই ফিল্টারের ক্লিক তখনই চলল, পর্দাও আঁকা হলো। একই মুহূর্তে Worker তার হিসাব চালিয়ে যাচ্ছে — দুটি core-এ দুটি কাজ।
  6. ফল আসে বার্তা হয়ে। Worker শেষ করে postMessage(ফল) করল। ফলটি main thread-এর task queue-তে একটি message event হিসেবে দাঁড়াল, আর পালা এলে onmessage callback পাতায় রিপোর্ট বসাল। মোট সময় প্রায় একই, কিন্তু পাতা একবারও জমেনি।
মূল ধারণা

Worker কাজকে দ্রুততর করে না — হিসাবটা প্রায় একই সময় নেয়। Worker কাজকে সরিয়ে দেয়, যাতে main thread সবসময় ব্যবহারকারীকে সাড়া দিতে পারে। লক্ষ্য দ্রুততা নয়, সচলতা — responsiveness.

২ · প্রথম Worker — new Worker, postMessage আর onmessage

সবচেয়ে সাধারণ ধরন হলো dedicated worker — একটি পাতা তাকে তৈরি করে, শুধু সেই পাতাই তার সাথে কথা বলে। দুটি ফাইল লাগে: পাতার নিজের কোড, আর Worker-এর কোড আলাদা একটি ফাইলে। আগের রিপোর্টের উদাহরণটাই লিখি:

// main.js — পাতার কোড (main thread)
const worker = new Worker("report-worker.js");

reportBtn.addEventListener("click", () => {
  spinner.hidden = false;
  worker.postMessage({ type: "sales-report", month: "2026-10" });
});

worker.addEventListener("message", (event) => {
  spinner.hidden = true;
  totalEl.textContent = `মোট বিক্রি: ${event.data.total} টাকা`;
});
// report-worker.js — আলাদা thread-এ চলে
self.addEventListener("message", (event) => {
  const { month } = event.data;
  const orders = loadOrders(month);        // ধরুন দশ লাখ অর্ডার
  let total = 0;
  for (const o of orders) total += o.price * o.qty;
  self.postMessage({ month, total });      // ফল ফেরত পাঠানো
});

এই দুটি ফাইল একটি আসল সাইটের জন্য — এখানে চালানোর জন্য নয় (এই পাতার কোড-রানার অন্য ফাইল লোড করতে পারে না; চালানো যায় এমন উদাহরণ নিচে আছে)। লক্ষ করুন তিনটি জিনিস। (১) new Worker(url) ফাইলটি ডাউনলোড করে আলাদা thread-এ চালায়; ফাইলটি পাতার একই origin থেকে আসতে হয়। (২) দুই দিকেই একই জোড়া: পাঠাতে postMessage(data), পেতে message event, যার event.data-তে পাঠানো ডেটার কপি। (৩) Worker-এর ভেতরে self মানে Worker নিজেই — সেখানে window বা document নেই। worker.onmessage = fn লেখাও চলে; addEventListener দিলে একাধিক listener রাখা যায়।

Module worker — import ব্যবহার করতে

পুরোনো ধরনের (classic) Worker-এ অন্য ফাইল আনতে হয় importScripts("a.js") দিয়ে, যা synchronous আর সব নাম global-এ ঢেলে দেয়। আধুনিক উপায় হলো Worker-কে ES module হিসেবে চালানো:

// পাতায়
const worker = new Worker(new URL("./report-worker.js", import.meta.url), { type: "module" });

// report-worker.js-এর ভেতরে এখন import চলে
import { sumOrders } from "./math.js";

{ type: "module" } দিলে Worker-এর ভেতরে import/export কাজ করে, আর importScripts আর চলে না (TypeError দেয়)। সব বড় ব্রাউজারে এটি এখন আছে — Firefox সবার শেষে, ২০২৩ সালে যুক্ত করেছে। new URL(…, import.meta.url) লেখার কারণ: Vite বা webpack-এর মতো bundler এই রূপটি চিনে Worker ফাইলটিকেও আলাদাভাবে bundle করে। Module নিয়ে বিস্তারিত পর্ব ৪৫-এ।

৩ · বার্তায় কী যায় — structured clone, তার খরচ আর transfer

Main thread আর Worker কোনো মেমরি ভাগ করে না। তাহলে postMessage({ orders }) করলে orders কীভাবে ওপারে যায়? ব্রাউজার ডেটাটির একটি গভীর কপি বানায় — পর্ব ১৯-এ দেখা structuredClone-এর একই নিয়মে, যার নাম structured clone algorithm. ফলে ওপারে যা পৌঁছায় তা সম্পূর্ণ আলাদা একটি object; Worker সেটি বদলালে main thread-এর আসলটা বদলায় না।

  • যা কপি হয়: সাধারণ object, array, string, number, BigInt, Date, RegExp, Map, Set, ArrayBuffer, typed array, Blob, File, ImageData, Error, এমনকি circular reference.
  • যা যায় না: function, DOM node — postMessage সাথে সাথে DataCloneError ছোড়ে। আর class-এর instance পৌঁছায় সাধারণ object হয়ে — method আর prototype হারিয়ে।
  • খরচ: কপি করতে সময় লাগে, আর সেই কপি বানানো (serialize) চলে পাঠানো thread-এই। কয়েক KB-র বার্তা টেরই পাবেন না; কিন্তু দশ লাখ object-এর array পাঠালে কপি করতেই main thread শত মিলিসেকেন্ড আটকে থাকতে পারে, আর মেমরিও দ্বিগুণ লাগে।

বড় বাইনারি ডেটার (ছবির pixel, অডিও, ফাইলের bytes) জন্য আছে আরেকটি পথ: transferable object. postMessage(msg, [buffer]) — দ্বিতীয় argument-এ যে ArrayBuffer-এর তালিকা দেবেন, সেগুলো কপি হয় না, শুধু মালিকানা বদলায়। কাজটি প্রায় তাৎক্ষণিক, ডেটা যত বড়ই হোক। দাম একটাই: পাঠানোর পর আপনার হাতের buffer-টি detached হয়ে যায় — byteLength হয় ০, আর তার ওপর বানানো typed array-ও খালি।

দুই thread-এর হাতে কী আছে
কোড
Main-এর হাতে
পথে
Worker-এর হাতে
মেমরি
  1. শুরুতে। Main thread-এর হাতে দুটি জিনিস: অর্ডারের একটি বড় array, আর একটি ছবির pixel-ভর্তি ৮ MB-র ArrayBuffer.
  2. Object পাঠানো = কপি। postMessage(orders) চলার সময় ব্রাউজার পুরো array ঘুরে ঘুরে serialize করে — এই সময়টা main thread-এর ঘাড়েই পড়ে। ডেটা বড় হলে এটাই নতুন long task হয়ে উঠতে পারে।
  3. দুই জায়গায় দুই কপি। Worker পেল একটি নতুন, স্বাধীন কপি। Worker সেটি বদলালে main-এর আসল array বদলায় না — কেউ কারও ডেটায় হাত দিতে পারে না বলে lock-এর দরকারও নেই।
  4. ArrayBuffer transfer. দ্বিতীয় argument-এ [pixels] দেওয়ায় কপি হলো না। ব্রাউজার শুধু bytes-এর মালিকানা Worker-কে দিল — ৮ KB হোক বা ৮০০ MB, সময় প্রায় শূন্য।
  5. পাঠানো দিক খালি। এখন main-এর pixels.byteLength হলো ০ — buffer detached. ভুল করে পড়তে গেলে কিছু পাবেন না। কাজ শেষে Worker একইভাবে transfer করে ফেরত দিতে পারে। এক মুহূর্তে মালিক একজনই — তাই এখানেও কোনো race নেই।

এবার নিজে চালিয়ে দেখুন। এই পাতার কোড-রানার অন্য ফাইল আনতে পারে না, তাই Worker-এর কোড একটি string থেকে Blob URL বানিয়ে দেওয়া হয়েছে — আসল প্রজেক্টে আলাদা ফাইলই ভালো। Main thread একটি ভারী কাজ পাঠায় (২,০০,০০০-এর নিচে কয়টি মৌলিক সংখ্যা), সাথে একটি ছোট ArrayBuffer transfer করে:

const workerCode = `
  self.onmessage = (e) => {
    const { limit, buf } = e.data;
    let count = 0;
    outer: for (let n = 2; n < limit; n++) {
      for (let d = 2; d * d <= n; d++) if (n % d === 0) continue outer;
      count++;
    }
    self.postMessage({ count, got: buf.byteLength });
  };`;
const url = URL.createObjectURL(new Blob([workerCode], { type: "text/javascript" }));
const worker = new Worker(url);
worker.onmessage = (e) => {
  console.log("৩. worker-এর উত্তর:", e.data);
  worker.terminate();
  URL.revokeObjectURL(url);
};
worker.onerror = (e) => console.log("worker error:", e.message);
const buf = new ArrayBuffer(1024);
worker.postMessage({ limit: 200_000, buf }, [buf]);
console.log("১. কাজ পাঠানো হলো; buf.byteLength এখন", buf.byteLength);
console.log("২. main thread মুক্ত — ক্লিক, আঁকা সব চলছে");

আউটপুট: ১. কাজ পাঠানো হলো; buf.byteLength এখন 0, তারপর ২. main thread মুক্ত…, শেষে ৩. worker-এর উত্তর: { count: 17984, got: 1024 }. তিনটি জিনিস দেখুন। (১) postMessage-এর ঠিক পরের লাইনেই main-এর buf খালি (০), অথচ Worker পেয়েছে পুরো ১০২৪ byte — কপি নয়, স্থানান্তর। (২) লাইন ১ আর ২ সবসময় উত্তরের আগে আসে: Worker যত দ্রুতই হিসাব করুক, তার উত্তর main thread-এ একটি task হিসেবে আসে, আর চলমান script শেষ না হলে কোনো task চলে না (পর্ব ৩১)। (৩) কাজ শেষে terminate() দিয়ে thread বন্ধ আর revokeObjectURL দিয়ে Blob URL মুক্ত করা হয়েছে। Edge-এ পরীক্ষা করা: এমন sandbox করা iframe-এও Blob URL থেকে Worker তৈরি হয়, কারণ Blob URL তৈরি করা পাতার origin-ই পায়।

সাবধান: transfer-এর তালিকায় buffer দিতে ভুলে গেলে কোনো error আসে না — শুধু চুপচাপ কপি হয়। ৫০ MB-র ভিডিও ফ্রেম প্রতিবার কপি হলে Worker ব্যবহারের পুরো লাভই হারিয়ে যায়। উল্টোটাও সত্য: transfer করার পর পুরোনো typed array পড়লে খালি পাবেন, তাই পাঠানোর পর সেই variable আর ব্যবহার করবেন না। postMessage(msg, { transfer: [buf] }) — option object দিয়েও একই কাজ লেখা যায়।

৪ · Worker-এর ভেতরের জগৎ — কী আছে, কী নেই, ভুল হলে কী হয়

Worker-এর global object হলো WorkerGlobalScope (dedicated worker-এ DedicatedWorkerGlobalScope), যাকে self বা globalThis দিয়ে পাওয়া যায়। এখানে ভাষার সব কিছু আছে — Promise, async/await, class, Map — আর অনেক Web API-ও আছে। নেই শুধু সেগুলো, যা পাতা বা জানালার সাথে জড়িত:

Worker-এ আছেWorker-এ নেই
fetch, WebSocket, setTimeout/setIntervalwindow, document, যেকোনো DOM node
IndexedDB, Cache APIlocalStorage, sessionStorage (এগুলো synchronous, তাই রাখা হয়নি)
crypto.subtle (hash, encryption), TextEncoder/TextDecoderalert, confirm, prompt
OffscreenCanvas, createImageBitmap, WebAssemblyপাতার variable বা function (সব আলাদা heap-এ)
navigator-এর কিছু অংশ, location (শুধু পড়ার জন্য), নতুন Worker তৈরিব্রাউজারের UI-এর কাজ — নতুন জানালা খোলা, scroll, ফোকাস বদলানো

DOM কেন নেই? কারণ DOM কোনো thread-safe কাঠামো নয়। দুই thread একসাথে একই node বদলাতে পারলে প্রতিটি node-এ lock লাগত — পর্ব ৩১-এর ভাবনার প্রশ্নে দেখেছেন, GUI-জগৎ ঠিক এটাই এড়াতে চায়। তাই নিয়ম: Worker হিসাব করে, main thread আঁকে। Worker ফল পাঠায়, আর main thread সেই ফল দিয়ে DOM বদলায়।

ভুল হলে — error আর messageerror

Worker-এর ভেতরে uncaught exception হলে Worker মরে না, কিন্তু পাতার কোডও কিছু জানে না — যদি আপনি না শোনেন। Main thread-এ Worker object-এ একটি error event আসে (ErrorEvent — message, filename, lineno সহ)। আর যদি কোনো বার্তা ওপারে গিয়ে আর deserialize করা না যায়, আসে messageerror event — এটি বিরল, কিন্তু চুপচাপ হারিয়ে যাওয়া বার্তার একমাত্র চিহ্ন।

worker.addEventListener("error", (e) => {
  e.preventDefault();               // পাতার window.onerror পর্যন্ত যাওয়া বন্ধ
  showToast(`রিপোর্ট বানানো গেল না: ${e.message}`);
});
worker.addEventListener("messageerror", () => showToast("বার্তা পড়া গেল না"));

// Worker-এর ভেতরে: নিজে ধরে কাঠামোবদ্ধ উত্তর পাঠানো আরও ভালো
self.onmessage = (e) => {
  try { self.postMessage({ ok: true, result: heavyWork(e.data) }); }
  catch (err) { self.postMessage({ ok: false, error: err.message }); }
};

দুই স্তরের সুরক্ষা। বাইরের error listener শেষ ভরসা — অপ্রত্যাশিত বাগ বা Worker ফাইল লোড না হলে কাজে আসে। ভেতরের try/catch প্রতিটি অনুরোধের উত্তরে জানিয়ে দেয় কাজটা সফল হলো কি না, যাতে main thread বুঝতে পারে কোন অনুরোধটি ব্যর্থ হয়েছে (পরের অংশের RPC-তে এটা কাজে লাগবে)। Error-এর বিস্তারিত পর্ব ২৯-এ।

বন্ধ করা — terminate() আর self.close()

একটি Worker নিজে থেকে বন্ধ হয় না; পাতা খোলা থাকা পর্যন্ত সে মেমরি ধরে রাখে। বন্ধ করার দুই উপায়: বাইরে থেকে worker.terminate() — সাথে সাথে থামায়, চলমান হিসাব মাঝপথে ফেলে দেয়, কোনো শেষ বার্তাও আসে না; আর ভেতর থেকে self.close() — Worker নিজের কাজ শেষ করে বিদায় নেয়। ব্যবহারকারী “বাতিল” চাপলে বা অন্য পাতায় চলে গেলে terminate() খুব কাজের — যেমন ১৫ সেকেন্ডের রিপোর্ট আর দরকার নেই, তাহলে CPU আর ব্যাটারি কেন খরচ হবে?

৫ · Worker-কে function-এর মতো ডাকা — id আর Promise দিয়ে ছোট RPC

এক ধরনের কাজ হলে onmessage-এ সরাসরি ফল বসানো চলে। কিন্তু বাস্তবে একই Worker-কে নানা কাজ দেওয়া হয়: রিপোর্ট, খোঁজা, export. একসাথে দুটি অনুরোধ পাঠালে আর ছোটটি আগে শেষ হলে, উত্তর আসে উল্টো ক্রমে। তখন কোন উত্তর কোন প্রশ্নের? সমাধান: প্রতিটি অনুরোধে একটি অনন্য id দিন, উত্তরেও সেই id ফেরত নিন, আর main thread-এ id অনুযায়ী অপেক্ষমাণ Promise পূর্ণ করুন। ফলে Worker-কে ডাকা যায় এভাবে: const r = await callWorker("report", data). একে বলে RPC (remote procedure call) — অন্য জায়গায় চলা কোডকে সাধারণ function-এর মতো ডাকা। Promise তৈরির খুঁটিনাটি পর্ব ৩৩-এ।

Main thread-এর pending Map আর Worker
ঘটনা
pending Map
Worker-এ চলছে
Promise-এর অবস্থা
  1. প্রথম অনুরোধ। callWorker নতুন id (1) বানাল, Promise-এর resolve function-টি Map-এ 1-এর নামে রাখল, তারপর { id: 1, type, payload } পাঠাল। ফেরত দিল একটি pending Promise.
  2. দ্বিতীয় অনুরোধ, অপেক্ষা ছাড়াই। প্রথমটির উত্তর আসার আগেই id 2 দিয়ে খোঁজার অনুরোধ গেল। দুটি কাজ এখন একসাথে “উড়ছে” — in flight.
  3. দ্রুতটি আগে ফিরল। খোঁজা ছোট কাজ, তাই { id: 2 } আগে এল। onmessage Map-এ 2 খুঁজে সেই resolve ডাকল আর entry মুছে দিল। খোঁজার Promise পূর্ণ।
  4. ধীরটি পরে। এবার { id: 1 } — Map থেকে report-এর resolve, Promise পূর্ণ, Map খালি। ক্রম উল্টো হলেও প্রত্যেকে নিজের উত্তর পেল।
  5. id না থাকলে। কেউ যদি ধরে নেয় “উত্তর আসবে পাঠানোর ক্রমে”, তাহলে রিপোর্টের জায়গায় বসত খোঁজার তালিকা — আর বাগটা ধরা পড়ত শুধু মাঝে মাঝে, যখন সময়ের হিসাব এমন হয়। এটি একটি ক্লাসিক race condition.

এবার পুরো ব্যবস্থাটা চালিয়ে দেখুন। Worker ইচ্ছা করে রিপোর্টে ৪০০ ms আর খোঁজায় ৫০ ms দেরি করে, যাতে উত্তর উল্টো ক্রমে আসে:

const src = `
  self.onmessage = ({ data: { id, type, payload } }) => {
    const result = type === "report"
      ? payload.reduce((sum, x) => sum + x, 0)
      : ["shirt", "shoe", "saree"].filter((p) => p.startsWith(payload));
    const delay = type === "report" ? 400 : 50;   // রিপোর্ট ধীর, খোঁজা দ্রুত
    setTimeout(() => self.postMessage({ id, result }), delay);
  };`;
const worker = new Worker(URL.createObjectURL(new Blob([src], { type: "text/javascript" })));
const pending = new Map(); let nextId = 1;        // id → resolve
worker.onmessage = ({ data: { id, result } }) => {
  console.log("উত্তর এল, id =", id);
  pending.get(id)(result);
  pending.delete(id);
};
const callWorker = (type, payload) => new Promise((resolve) => {
  const id = nextId++;
  pending.set(id, resolve);
  worker.postMessage({ id, type, payload });
});
callWorker("report", [1200, 850, 430]).then((r) => { console.log("রিপোর্ট:", r); worker.terminate(); });
callWorker("search", "sh").then((r) => console.log("খোঁজা:", r));

আউটপুট: উত্তর এল, id = 2 → খোঁজা: [ 'shirt', 'shoe' ] → উত্তর এল, id = 1 → রিপোর্ট: 2480. রিপোর্ট আগে চাওয়া হলেও খোঁজার উত্তর আগে এসেছে, আর id-র কারণে প্রত্যেক .then ঠিক নিজের ফল পেয়েছে। “উত্তর এল” লেখাটি সবসময় ফলের আগে, কারণ resolve ডাকলে then-এর callback সাথে সাথে চলে না — একটি microtask হিসেবে onmessage শেষ হওয়ার পরে চলে। বাস্তবে আরও তিনটি জিনিস যোগ করবেন: Map-এ resolve-এর সাথে reject-ও রাখা (Worker { ok: false } পাঠালে reject), একটি timeout, আর Worker-এর error event-এ সব pending Promise reject করা — নইলে সেগুলো চিরকাল ঝুলে থাকবে।

Comlink: এই id-Map-Promise-এর কাজটা হাতে না লিখে অনেকে Google Chrome Labs-এর ছোট library Comlink ব্যবহার করেন। Worker-এ Comlink.expose(api), পাতায় const api = Comlink.wrap(worker) — তারপর সরাসরি await api.salesReport(month). ভেতরে সে Proxy (পর্ব ২৮) দিয়ে প্রতিটি method-ডাককে বার্তায় বদলে দেয়। সুবিধাজনক, তবে মনে রাখুন: প্রতিটি ডাক তবুও একটি বার্তা, আর প্রতিটি argument তবুও কপি হয়।

৬ · বাস্তব কাজে Worker — আর কখন Worker দরকার নেই

কোন কাজ Worker-এ যাওয়ার যোগ্য? যে কাজ CPU-bound (অপেক্ষা নয়, হিসাব করে সময় নেয়), DOM-এর দরকার নেই, আর শত মিলিসেকেন্ড বা তার বেশি লাগে। কয়েকটি বাস্তব উদাহরণ:

🔎 টাইপ করতে করতে খোঁজা

একটি দোকানের ১,০০,০০০ পণ্য Worker-এ একবার পাঠিয়ে রাখুন। প্রতিবার টাইপে শুধু খোঁজার শব্দ যায়, Worker ফিল্টার করে প্রথম ৫০টি ফল ফেরত দেয়। ইনপুট বাক্স কখনো আটকায় না; পর্ব ৩৬-এর debounce আর পুরোনো অনুরোধ বাতিল করা এখানে একসাথে কাজে লাগে।

বড় ডেটা একবার, ছোট প্রশ্ন বারবার

🖼️ ছবিতে ফিল্টার

Canvas থেকে getImageData নিয়ে তার data.buffer transfer করুন। Worker প্রতিটি pixel-এ grayscale-এর হিসাব করে (যেমন 0.299R + 0.587G + 0.114B), buffer আবার transfer করে ফেরত দেয়, main thread শুধু putImageData করে।

transfer দিয়ে যাওয়া-আসা

🔐 আপলোডের আগে ফাইলের hash

ব্যবহারকারী ২ GB-র ভিডিও বাছলেন। File object Worker-এ পাঠান (File-ও কপি করা যায়, ভেতরের bytes নয় — শুধু হাতল), Worker টুকরো টুকরো পড়ে hash বানায়; ছোট ফাইলে সরাসরি crypto.subtle.digest("SHA-256", buffer) (এটি টুকরো টুকরো hash করতে পারে না, তাই বড় ফাইলে একটি hash library লাগে)। ফলে একই ফাইল দ্বিতীয়বার আপলোড ঠেকানো যায়।

বড় ফাইল, পাতা সচল

📊 দশ লাখ অর্ডারের রিপোর্ট

অংশ ১-এর উদাহরণ: IndexedDB থেকে Worker নিজেই ডেটা পড়ে (IndexedDB Worker-এ আছে), যোগফল আর গ্রুপ করে, শুধু ছোট সারাংশ main-এ পাঠায়। বিশাল ডেটা কখনো main thread ছোঁয়ই না — কপির খরচও নেই।

ডেটা যেখানে, হিসাব সেখানে

Worker pool — একাধিক core কাজে লাগানো

একটি Worker মানে একটি বাড়তি core. ২০টি ছবির থাম্বনেইল বানাতে হলে কয়েকটি Worker একসাথে চালানো যায় — একে বলে worker pool, Node.js-এর libuv-এর thread pool-এর মতোই চিন্তা। ব্রাউজার জানায় যন্ত্রে কয়টি logical core আছে: navigator.hardwareConcurrency. সাধারণ নিয়ম: core-এর সংখ্যা থেকে একটি কম (main thread-এর জন্য), আর একটি যুক্তিসঙ্গত সর্বোচ্চ সীমা। প্রতিটি কাজের জন্য নতুন Worker নয় — pool একবার বানিয়ে বারবার ব্যবহার।

Pool-এর অবস্থা (hardwareConcurrency = 4, তাই ৩টি Worker)
কাজের লাইন
Worker A
Worker B
Worker C
Main thread
  1. Pool তৈরি। hardwareConcurrency বলছে ৪টি core. Main thread-এর জন্য একটি রেখে ৩টি Worker বানানো হলো — একবারই, শুরুতে। ৮টি ছবি লাইনে।
  2. প্রত্যেকে একটি করে। ফাঁকা তিন Worker-কে প্রথম তিনটি ছবি দেওয়া হলো। Main thread তিনটি বার্তা পাঠিয়েই মুক্ত।
  3. যে আগে ফাঁকা, সে পরেরটা। Worker B ছবি ২ শেষ করে ফল পাঠাল; pool সাথে সাথে তাকে লাইনের পরের ছবি (৪) দিল। কোনো Worker বসে থাকে না, আর কাজও ক্রমানুসারে শেষ হয় না — তাই আবারও id লাগে।
  4. সব শেষ। ৮টি ফল এসেছে; main thread সেগুলো id অনুযায়ী সাজিয়ে দেখাল। ৩টি core-এ কাজ প্রায় ৩ গুণ দ্রুত শেষ হলো — তবে কখনোই পুরো ৩ গুণ নয় (ভাবনার প্রশ্ন ৩-এ Amdahl-এর সূত্র দেখুন)।

কখন Worker-এর খরচ লাভের চেয়ে বেশি

  • ছোট কাজ: একটি Worker চালু হতে (ফাইল আনা, parse, নতুন JavaScript পরিবেশ) কয়েক থেকে কয়েক দশ মিলিসেকেন্ড আর কয়েক MB মেমরি লাগে। প্রতিটি বার্তায় কপির খরচ আছে। ৫ ms-এর হিসাব Worker-এ পাঠালে আসা-যাওয়াতেই বেশি সময় যায়।
  • DOM-নির্ভর কাজ: ধীরতার কারণ যদি হাজারটা DOM node বানানো বা layout হয়, Worker সাহায্য করতে পারে না — সেই কাজ main thread-এরই। সেখানে কম node বানানো, virtual list, বা পর্ব ৩১-এর chunking.
  • অপেক্ষা-প্রধান কাজ: fetch বা timer-এর অপেক্ষা এমনিতেই main thread আটকায় না। শুধু নেটওয়ার্কের জন্য Worker লাগে না।
  • বিশাল ডেটার বারবার আসা-যাওয়া: প্রতিবার ৫০ MB-র object কপি হলে লাভ শেষ। ডেটা Worker-এই রাখুন, বা transfer করুন।
সিদ্ধান্তের প্রশ্ন

আগে মাপুন — DevTools দিয়ে মাপা শিখবেন পর্ব ৫৩-এ। তারপর জিজ্ঞেস করুন: (১) কাজটা কি ৫০ ms-এর বেশি CPU নেয়? (২) DOM ছাড়া কি করা যায়? (৩) আসা-যাওয়ার ডেটা কি ছোট, বা transfer করা যায়? তিনটিরই উত্তর “হ্যাঁ” হলে Worker. না হলে আগে অ্যালগরিদম ভালো করা বা chunking.

৭ · আরও গভীরে — shared worker, SharedArrayBuffer, OffscreenCanvas আর Node.js

Shared worker

Shared worker (new SharedWorker("hub.js")) একই origin-এর একাধিক tab বা iframe ভাগ করে ব্যবহার করে। প্রতিটি সংযোগ পায় একটি port, আর Worker-এর ভেতরে connect event দিয়ে নতুন tab চেনা যায়। ব্যবহার: পাঁচটি tab খোলা থাকলেও সার্ভারের সাথে একটিমাত্র WebSocket সংযোগ রাখা। সমর্থন সমান নয় — বিশেষ করে Android-এর Chrome-এ দীর্ঘদিন এটি ছিল না — তাই feature detection আর fallback রাখতে হয়।

SharedArrayBuffer আর Atomics — সত্যিকারের ভাগ করা মেমরি

এতক্ষণের নিয়ম ছিল “কিছুই ভাগ হয় না”। ব্যতিক্রম SharedArrayBuffer: এটি postMessage করলে কপি বা transfer হয় না — দুই thread একই bytes দেখে। একজন লিখলে অন্যজন সাথে সাথে দেখে। তখনই ফিরে আসে C++ আর Java-র পুরোনো সমস্যা — দুজন একসাথে লিখলে ডেটা নষ্ট। তাই সাথে আছে Atomics: Atomics.add, Atomics.compareExchange ভাঙা যায় না এমন এক ধাপে পড়া-লেখা করে, আর Atomics.wait/Atomics.notify দিয়ে এক thread অন্যটির জন্য ঘুমিয়ে অপেক্ষা করতে পারে (main thread-এ wait নিষিদ্ধ — পাতা আটকে যাবে)।

২০১৮ সালে Spectre আবিষ্কারের পর ব্রাউজারগুলো SharedArrayBuffer বন্ধ করে দিয়েছিল (ভাবনার প্রশ্ন ২)। এখন এটি পাওয়া যায় শুধু পাতা cross-origin isolated হলে — সার্ভারকে এই দুটি HTTP header পাঠাতে হয়:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

প্রথমটি (COOP) আপনার পাতাকে অন্য origin-এর জানালা (popup বা যে পাতা আপনাকে খুলেছে) থেকে আলাদা করে — কেউ কারও window-এর reference পায় না, ফলে ব্রাউজার পাতাটিকে আলাদা process-এ রাখতে পারে; দ্বিতীয়টি (COEP) বলে, যে অন্য-origin ফাইল (ছবি, script, iframe) স্পষ্ট অনুমতি দেয়নি, তা লোড হবে না। কোডে যাচাই: self.crossOriginIsolated === true. দাম আছে — অনেক বিজ্ঞাপন বা বাইরের embed এতে ভেঙে যায়। এটি উন্নত বিষয়; বেশিরভাগ অ্যাপের জন্য সাধারণ বার্তা আর transfer-ই যথেষ্ট। নিরাপত্তার header নিয়ে আরও কথা পর্ব ৪৩-এ।

OffscreenCanvas আর WebAssembly

OffscreenCanvas দিয়ে আঁকার কাজও Worker-এ নেওয়া যায়: canvas.transferControlToOffscreen() একটি OffscreenCanvas দেয়, সেটি Worker-এ transfer করলে Worker সরাসরি পর্দার সেই canvas-এ আঁকে — চার্ট, ম্যাপ বা গেমের রেন্ডারিং main thread-এর ভিড় থেকে মুক্ত থাকে। আর WebAssembly-র ভারী হিসাব (ভিডিও encode, PDF তৈরি, ছবি সম্পাদনা) প্রায় সবসময় Worker-এ চলে; একাধিক Worker আর SharedArrayBuffer মিলিয়ে WebAssembly thread — C/C++-এর pthread কোড ব্রাউজারে চলে এভাবেই।

Node.js-এ — worker_threads, cluster আর child_process

Node.js-এ একই চিন্তার নাম worker_threads (Node 12 থেকে stable). ব্রাউজারের মতোই আলাদা thread, আলাদা event loop, বার্তা দিয়ে যোগাযোগ, transfer আর SharedArrayBuffer — শুধু নামগুলো আলাদা: parentPort, workerData, আর event শোনা হয় .on("message") দিয়ে।

const { Worker } = require("node:worker_threads");

const code = `
  const { parentPort, workerData } = require("node:worker_threads");
  let total = 0;
  for (let i = 1; i <= workerData.count; i++) total += i % 1000; // নকল অর্ডারের দাম
  parentPort.postMessage(total);
`;
const w = new Worker(code, { eval: true, workerData: { count: 1_000_000 } });
w.on("message", (total) => console.log("worker-এর যোগফল:", total));
w.on("error", (err) => console.error("worker error:", err));
w.on("exit", (exitCode) => console.log("worker বন্ধ, exit code", exitCode));
console.log("main thread মুক্ত");

// প্রত্যাশিত আউটপুট:
// main thread মুক্ত
// worker-এর যোগফল: 499500000
// worker বন্ধ, exit code 0

এখানে Worker-এর কোড string হিসেবে দেওয়া হয়েছে (eval: true) যাতে এক ফাইলেই দেখা যায়; আসল প্রজেক্টে আলাদা ফাইলের path দেবেন। workerData দিয়ে শুরুর ডেটা যায়, সেটিও structured clone হয়ে। Worker-এর কাজ শেষ হলে আর কোনো অপেক্ষার কাজ না থাকায় সে নিজে বন্ধ হয় — exit event আসে কোড ০ নিয়ে। ব্রাউজারের Worker এভাবে নিজে বন্ধ হয় না, খেয়াল করুন। পর্ব ৩১-এ দেখেছেন, Node সার্ভারের request handler-এ ভারী sync কাজ পুরো সার্ভার আটকায়; সেই কাজ worker_threads-এ পাঠানো হলো সমাধানের একটি পথ।

Node-এ আরও দুটি পুরোনো উপায় আছে, যা thread নয়, আলাদা process: child_process দিয়ে অন্য প্রোগ্রাম (যেমন ffmpeg বা একটি Python script) চালানো, আর cluster দিয়ে একই সার্ভারের কয়েকটি কপি চালিয়ে একটি port ভাগ করা — প্রতিটি core-এ একটি process, প্রতিটির নিজের মেমরি। মোটা দাগে: একটি ভারী হিসাব → worker_threads; বাইরের প্রোগ্রাম → child_process; বেশি request সামলাতে সব core ব্যবহার → cluster, বা PM2-র মতো টুল, বা কয়েকটি container. Node-এর বিস্তারিত পর্ব ৪৯-এ।

তিন রকম “worker” — গুলিয়ে ফেলবেন না

Web Worker (dedicated / shared)Service workerWorklet
কাজভারী হিসাব main thread থেকে সরানোপাতা আর নেটওয়ার্কের মাঝে proxy — অফলাইন cache, push notificationব্রাউজারের rendering বা অডিও pipeline-এর ভেতরে ছোট hook
কে চালায়আপনার পাতা, new Worker() দিয়েব্রাউজার, register করার পর ঘটনা এলে (fetch, push)ব্রাউজার, যখন দরকার (প্রতি অডিও ব্লক, প্রতি paint)
আয়ুপাতা খোলা থাকা পর্যন্ত বা terminate পর্যন্তপাতা বন্ধ হলেও থাকতে পারে; ফাঁকা থাকলে ব্রাউজার থামিয়ে দেয়যে কাজের জন্য, তার সাথেই
DOMনেইনেইনেই
উদাহরণরিপোর্ট, ছবি ফিল্টার, খোঁজাPWA-র অফলাইন মোড — পর্ব ৪৪AudioWorklet, CSS Paint API (paintWorklet)

সংক্ষেপে: service worker নেটওয়ার্কের পাহারাদার, Web Worker হিসাবের সহকারী, আর worklet ব্রাউজারের নিজের যন্ত্রের ভেতরে লাগানো ছোট যন্ত্রাংশ। তিনটিই আলাদা global scope-এ চলে আর DOM ছুঁতে পারে না — মিল ওইটুকুই।

C/C++/Python/Java জানলে

C/C++-এ pthread বা std::thread, আর Java-র Thread — সব thread একই মেমরি ভাগ করে, তাই mutex, lock আর synchronized দিয়ে race ঠেকাতে হয় (C++-এ মাল্টিথ্রেডিং, Java-র thread আর synchronization পর্ব দেখুন)। Python-এ GIL-এর কারণে সাধারণ CPython-এ thread একসাথে Python কোড চালায় না; CPU-ভারী কাজে multiprocessing লাগে, যেখানে process-গুলো ডেটা pickle করে কপি পাঠায় — JavaScript Worker-এর সবচেয়ে কাছের আত্মীয় (Python-এর thread, process আর GIL পর্ব দেখুন)। JavaScript Worker ডিফল্টভাবে কিছুই ভাগ করে না; SharedArrayBuffer না নিলে lock-এর দরকারই পড়ে না।

পরীক্ষা ও ইন্টারভিউতে
  • Web Worker কী, আর কেন লাগে? — আলাদা thread-এ JavaScript চালানোর ব্যবস্থা; CPU-ভারী কাজ main thread থেকে সরিয়ে পাতা সচল রাখে।
  • Worker কি DOM বদলাতে পারে? — না। Worker ফল বার্তায় পাঠায়, main thread DOM বদলায়।
  • postMessage-এ ডেটা কীভাবে যায়? — structured clone দিয়ে কপি হয়; function পাঠালে DataCloneError. ArrayBuffer transfer করলে কপি ছাড়া সরে যায়, পাঠানো দিকেরটা detached হয়।
  • Web Worker আর service worker-এর পার্থক্য? — Web Worker পাতার হিসাবের সহকারী, পাতার সাথে বাঁচে; service worker নেটওয়ার্কের proxy, পাতা বন্ধ হলেও থাকতে পারে, অফলাইন আর push-এর জন্য।
  • Node.js-এ CPU-ভারী কাজ request আটকাচ্ছে — কী করবেন? — কাজটি worker_threads-এ (বা একটি worker pool-এ) পাঠানো; অনেক request সামলাতে cluster.

৮ · AI-এর যুগে — Worker নিয়ে AI-এর পরিচিত ভুল

“পাতা ধীর, Worker দিয়ে ঠিক করো” — এমন অনুরোধে AI দ্রুত কোড দেয়, কিন্তু প্রায়ই নিচের ভুলগুলো করে। এগুলো চিনে রাখলে আপনার রিভিউ অনেক দ্রুত হবে:

AI যা লেখেসমস্যা ও সমাধান
কয়েক মিলিসেকেন্ডের কাজের জন্যও Worker, প্রতিবার নতুন new Worker()চালু হওয়া আর কপির খরচ লাভের চেয়ে বেশি; আগে মাপুন, Worker একবার বানিয়ে বারবার ব্যবহার করুন
Worker-এর ভেতরে document.querySelector বা localStorageWorker-এ এগুলো নেই — ReferenceError. ফল বার্তায় পাঠান, DOM বদলান main thread-এ; storage দরকার হলে IndexedDB
বড় ArrayBuffer বা ImageData transfer-এর তালিকা ছাড়া পাঠানোচুপচাপ পুরো কপি — main thread আবার আটকায়; postMessage(msg, [buf])
বার্তায় function বা class instance পাঠানোfunction-এ DataCloneError; class instance পৌঁছায় method ছাড়া — সাধারণ ডেটা পাঠান
error listener নেই, Worker-এর ভেতরে try/catch নেইভুল হলে Promise চিরকাল ঝুলে থাকে, spinner থামে না; দুই স্তরেই error সামলান
কাজ শেষ বা বাতিল হলেও terminate() নেই, component সরানোর সময়ও নাপুরোনো Worker মেমরি আর CPU খায়; React-এর মতো framework-এ cleanup-এ terminate করুন
একাধিক অনুরোধে id ছাড়া “পরের উত্তরটাই আমার”উত্তর উল্টো ক্রমে এলে ভুল ফল — id দিয়ে মেলান

AI-কে Worker-এর কোড লিখতে দিলে সীমাগুলো আগেই বলে দিন — তাতে উপরের বেশিরভাগ ভুল শুরুতেই আটকায়:

আমার ওয়েব অ্যাপে এই কাজটি main thread আটকে দিচ্ছে: [কাজের বর্ণনা ও বর্তমান কোড — কত ডেটা, কত সময় লাগে] ১. আগে বলো, কাজটি Worker-এ নেওয়ার যোগ্য কি না (CPU-ভারী? DOM লাগে? ডেটা কত বড়?). যোগ্য না হলে বিকল্প বলো (chunking, অ্যালগরিদম বদল)। ২. যোগ্য হলে একটি module worker লেখো ({ type: "module" })। Worker-এর ভেতরে কোনো DOM, window বা localStorage ব্যবহার করবে না। ৩. callWorker(type, payload) নামে একটি Promise-ভিত্তিক wrapper দাও — প্রতিটি অনুরোধে id, resolve আর reject দুটোই, timeout, আর Worker-এর error event-এ সব pending Promise reject করো। ৪. বড় বাইনারি ডেটা (ArrayBuffer, ImageData) transfer করো, কপি নয়; কোন variable পাঠানোর পর আর ব্যবহার করা যাবে না, মন্তব্যে লেখো। ৫. Worker একবার তৈরি করে বারবার ব্যবহার করো, আর কাজ বাতিল বা পাতা ছাড়ার সময় terminate() করো। ৬. কোন ব্রাউজারে কোন API (module worker, OffscreenCanvas, SharedArrayBuffer) নেই বা বিশেষ header লাগে, স্পষ্ট বলো।

AI-এর Worker কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Web Worker-এর নকশাকারীরা Java বা C++-এর মতো ভাগ করা মেমরির thread না দিয়ে কেন “কিছুই ভাগ নয়, শুধু বার্তা” বেছে নিলেন?

Web Worker-এর spec লেখা হয় HTML5-এর কাজের সময়, WHATWG-তে (মূল সম্পাদক Ian Hickson), আর ২০০৯ সালে Firefox 3.5, Safari 4 আর পরে Chrome-এ এটি আসে। তখন ওয়েবের সামনে একটি কঠিন প্রশ্ন ছিল: কোটি কোটি পাতায় লেখা কোড — যার বেশিরভাগ লেখকই thread-এর বিপদ সম্পর্কে কিছু জানেন না — তাদের হাতে কীভাবে parallelism দেওয়া যায়, অথচ ব্রাউজার নিজে না ভাঙে? Java আর C++-এর অভিজ্ঞতা স্পষ্ট ছিল: ভাগ করা মেমরি আর lock মানে race condition, deadlock, আর এমন বাগ যা হাজারবারে একবার ঘটে। তার ওপর JavaScript engine-গুলো (তখনকার SpiderMonkey, JavaScriptCore) একটি thread ধরে নিয়েই বানানো; প্রতিটি object-এ lock বসানো মানে পুরো engine নতুন করে লেখা।

সমাধানটি এসেছিল অনেক পুরোনো একটি ধারণা থেকে — actor model. ১৯৭৩ সালে Carl Hewitt (Peter Bishop আর Richard Steiger-এর সাথে) এই মডেল প্রস্তাব করেন: প্রতিটি “actor” নিজের অবস্থা নিজে রাখে, অন্য কেউ তা সরাসরি ছুঁতে পারে না; actor-রা শুধু একে অপরকে বার্তা পাঠায়, আর প্রত্যেকে নিজের বার্তার লাইন একটি একটি করে সামলায়। ১৯৮০-র দশকে Ericsson-এ Joe Armstrong ও তাঁর সহকর্মীরা টেলিফোন সুইচের জন্য Erlang ভাষা বানান ঠিক এই চিন্তায় — হাজার হাজার হালকা process, কিছুই ভাগ নয়, শুধু বার্তা; একটি ভাঙলে বাকিরা চলতে থাকে। পরে Go ভাষার একটি বিখ্যাত প্রবাদও একই কথা বলে: “মেমরি ভাগ করে যোগাযোগ কোরো না; যোগাযোগ করে মেমরি ভাগ করো।”

একটি Web Worker হুবহু একটি actor: নিজের heap, নিজের event loop (বার্তার লাইন), একটি একটি করে বার্তা সামলানো, আর বাইরের কারও সাথে মেমরি ভাগ নেই। ফলে run-to-completion নিয়ম Worker-এর ভেতরেও অটুট থাকে — যে কোড লেখা হয় একটি thread-এর মতো করে, সেটি নিরাপদ থাকে। দামটা আমরা এই পর্বে দেখেছি: প্রতিটি বার্তায় কপির খরচ। সেই দাম কমাতেই পরে এসেছে transferable object, আর অনেক পরে, কঠোর শর্তে, SharedArrayBuffer.

মূল উপলব্ধি: Web Worker-এর নকশা একটি সচেতন বিনিময় — কিছু কার্যক্ষমতা ছেড়ে দিয়ে নিরাপত্তা আর সরলতা কেনা। ভাগ করা মেমরি দ্রুত, কিন্তু ভুল করা সহজ; বার্তা ধীর, কিন্তু ভুল করা কঠিন। লাখো সাধারণ ডেভেলপারের প্ল্যাটফর্মে দ্বিতীয়টিই সঠিক ডিফল্ট।

প্র ০২২০১৮ সালের জানুয়ারিতে সব বড় ব্রাউজার হঠাৎ SharedArrayBuffer বন্ধ করে দিল। একটি CPU-র নকশার ত্রুটির সাথে JavaScript-এর একটি API-র কী সম্পর্ক?

SharedArrayBuffer আর Atomics ES2017-এ যুক্ত হয়, আর ২০১৭ সালে ব্রাউজারগুলোতে চালু হয় — উদ্দেশ্য ছিল WebAssembly-তে C/C++-এর multithreaded কোড চালানো। কয়েক মাস পরেই, ২০১৮ সালের জানুয়ারির শুরুতে প্রকাশ পায় Spectre আর Meltdown — আধুনিক CPU-র “speculative execution” কৌশলের দুর্বলতা। CPU গতি বাড়াতে আগে থেকে অনুমান করে কিছু নির্দেশ চালিয়ে ফেলে; অনুমান ভুল হলে ফল ফেলে দেয়, কিন্তু cache-এ তার ছাপ থেকে যায়। কোন মেমরি cache-এ আছে আর কোনটা নেই — এটা মাপা যায় পড়তে কত ন্যানোসেকেন্ড লাগল তা দেখে। আর সেই সূক্ষ্ম সময় থেকে আক্রমণকারী অন্য কোডের গোপন ডেটা অনুমান করতে পারে।

ব্রাউজারে এর মানে ছিল ভয়ংকর: একটি বিজ্ঞাপনের script হয়তো একই process-এ থাকা অন্য সাইটের ডেটা (পাসওয়ার্ড, cookie) পড়ে ফেলতে পারে। আক্রমণের মূল হাতিয়ার একটি খুব সূক্ষ্ম ঘড়ি। ব্রাউজারগুলো সাথে সাথে performance.now()-এর নির্ভুলতা কমিয়ে দেয়। কিন্তু SharedArrayBuffer দিয়ে নিজেই একটি ঘড়ি বানানো যায়: একটি Worker অবিরাম একটি shared সংখ্যা বাড়াতে থাকে, আর আরেকটি thread সেটি পড়ে — এটি প্রায় ন্যানোসেকেন্ড-স্তরের টাইমার। তাই ২০১৮ সালের জানুয়ারিতে কয়েক দিনের মধ্যে Chrome, Firefox, Safari, Edge সবাই SharedArrayBuffer বন্ধ করে দেয়।

ফেরার পথ ছিল দীর্ঘ। Chrome ডেস্কটপে site isolation (প্রতিটি সাইট আলাদা process-এ) চালু করে ২০১৮ সালেই আবার চালু করে। কিন্তু স্থায়ী সমাধান হলো cross-origin isolation: COOP আর COEP header দিয়ে পাতা ঘোষণা করে যে অন্য origin-এর কোনো জিনিস তার process-এ অনুমতি ছাড়া ঢুকবে না। তখন আক্রমণকারীর পড়ার মতো কিছুই সেই process-এ থাকে না। Firefox ২০২০ সালে এই শর্তে আবার চালু করে, আর Chrome ২০২১ সালে (Chrome 92) শর্তটি বাধ্যতামূলক করে।

মূল উপলব্ধি: নিরাপত্তা শুধু কোডের বাগ নয় — হার্ডওয়্যারের গভীরের আচরণও কোনো সফটওয়্যার API-কে বিপজ্জনক বানিয়ে দিতে পারে। আর একটি সূক্ষ্ম ঘড়ি, যা শুনতে নিরীহ, সেটিই হতে পারে আক্রমণের মূল যন্ত্র।

প্র ০৩আপনার ল্যাপটপে ৮টি core. ৮টি Worker দিলে কাজ কি ৮ গুণ দ্রুত হবে? Amdahl-এর সূত্র কী বলে?

১৯৬৭ সালে IBM-এর কম্পিউটার স্থপতি Gene Amdahl একটি সম্মেলনে একটি সরল কিন্তু গভীর যুক্তি দেন, যা পরে Amdahl-এর সূত্র নামে পরিচিত হয়। ধরুন একটি কাজের p অংশ ভাগ করে একসাথে করা যায়, আর বাকি (1 − p) অংশ শুধু একজনকেই একটির পর একটি করতে হয়। n টি প্রসেসর দিলে সবচেয়ে বেশি গতি বাড়বে 1 / ((1 − p) + p / n) গুণ। p = 0.9 হলে (৯০% কাজ ভাগ করা যায়) ৮টি core-এ গতি হয় প্রায় ৪.৭ গুণ, ৮ নয়; আর অসীম core দিলেও সর্বোচ্চ ১০ গুণ — কারণ ওই ১০% ক্রমিক অংশ কখনো ছোট হয় না।

Worker-এর জগতে এই ক্রমিক অংশ কোথায়? ডেটা ভাগ করে পাঠানো, structured clone-এর কপি, প্রতিটি Worker চালু হওয়া, আর শেষে সব ফল এক জায়গায় মেলানো — এগুলো সবই main thread-এ, একটির পর একটি হয়। তার ওপর যন্ত্রের core-গুলো আপনার একার নয়: ব্রাউজারের নিজের thread (rendering, GPU, নেটওয়ার্ক), অন্য tab, অন্য অ্যাপ — সবাই ভাগ চায়। বাংলাদেশে অনেকের হাতে থাকা কম দামের ফোনে core-গুলোর মধ্যে কয়েকটি ধীর “efficiency core”, আর গরম হলে CPU নিজেই গতি কমিয়ে দেয়। তাই hardwareConcurrency যত বলে, বাস্তব লাভ প্রায় সবসময় তার কম।

১৯৮৮ সালে John Gustafson একটি গুরুত্বপূর্ণ পাল্টা দৃষ্টিভঙ্গি দেন: বাস্তবে বেশি প্রসেসর পেলে মানুষ একই কাজ দ্রুত করে না, বরং একই সময়ে বড় কাজ করে — বড় ছবি, বেশি ডেটা। তখন ভাগ করা যায় এমন অংশ বাড়ে, আর লাভও বাড়ে। দুটো দৃষ্টিভঙ্গিই সত্যি, শুধু প্রশ্নটা আলাদা।

মূল উপলব্ধি: parallelism-এর সীমা ঠিক করে কাজের সবচেয়ে ধীর, ভাগ-না-করা অংশ। Worker বাড়ানোর আগে জিজ্ঞেস করুন: ডেটা পাঠানো আর ফল মেলানোয় কত সময় যায়? অনেক সময় এক-দুইটি Worker আর ভালো অ্যালগরিদম আটটি Worker-এর চেয়ে বেশি কাজের।

প্র ০৪“The main thread is overworked & underpaid” — ২০১৯ সালের এই বক্তৃতা কেন বলেছিল, অ্যাপের বেশিরভাগ যুক্তিই main thread থেকে সরানো উচিত?

২০১৯ সালের Chrome Dev Summit-এ Google Chrome দলের ডেভেলপার অ্যাডভোকেট Surma এই শিরোনামে একটি বক্তৃতা দেন। তাঁর মূল যুক্তি ছিল যন্ত্রের বৈচিত্র্য নিয়ে। ডেভেলপাররা অ্যাপ বানান ও পরীক্ষা করেন দামি ল্যাপটপ বা নতুন ফ্ল্যাগশিপ ফোনে; অথচ পৃথিবীর বিপুল সংখ্যক মানুষ — বাংলাদেশসহ — ওয়েব ব্যবহার করেন কম দামের Android ফোনে, যার একটি core হয়তো দামি ফোনের চেয়ে কয়েক গুণ ধীর। একই JavaScript কাজ যা আপনার যন্ত্রে ২০ ms, তা সেখানে ১০০ ms ছাড়িয়ে যেতে পারে। আর একটি ফ্রেম আঁকার বাজেট মাত্র প্রায় ১৬ ms (সেকেন্ডে ৬০ ফ্রেম)।

সমস্যা হলো, main thread-এর কাজ কতক্ষণ লাগবে তা আপনি নিয়ন্ত্রণ করতে পারেন না — ব্যবহারকারীর যন্ত্র ঠিক করে। কিন্তু main thread-এ কী চলবে, তা আপনি ঠিক করতে পারেন। তাই প্রস্তাব: main thread-কে রাখুন শুধু UI-এর জন্য — DOM বদলানো, ইনপুট নেওয়া, অ্যানিমেশন। বাকি সব — অ্যাপের state, ব্যবসার নিয়ম, ডেটা প্রসেসিং, এমনকি গেমের যুক্তি — Worker-এ। একে বলা হয় off-main-thread architecture; এর ভিত্তি সেই actor model, যেখানে UI একটি actor, state আরেকটি, আর তারা বার্তা দিয়ে কথা বলে। এই চিন্তাকে সহজ করতে Surma নিজেই Comlink library বানিয়েছিলেন, আর Google-এর ছবি ছোট করার অ্যাপ Squoosh-এ এভাবে ভারী কাজ Worker-এ চলে।

সমালোচনাও আছে: প্রতিটি state পরিবর্তন বার্তায় যাওয়া-আসা করলে কোড জটিল হয়, debug কঠিন হয়, আর কপির খরচ ছোট আপডেটেও যোগ হয়। তাই বাস্তবে বেশিরভাগ দল পুরো অ্যাপ নয়, শুধু ভারী অংশগুলো Worker-এ নেয়। তবু বক্তৃতার মূল বার্তাটি — main thread একটি দুর্লভ, ভাগ করা সম্পদ — INP আসার পর আরও প্রাসঙ্গিক হয়েছে।

মূল উপলব্ধি: আপনার যন্ত্র আপনার ব্যবহারকারীর যন্ত্র নয়। যে কাজ UI নয়, তা main thread-এ থাকার কোনো অধিকার নেই — এই প্রশ্নটা প্রতিটি নতুন feature-এ একবার করে জিজ্ঞেস করুন।

প্র ০৫Figma বা Photoshop-এর মতো ভারী ডিজাইন সফটওয়্যার এখন ব্রাউজারেই চলে। WebAssembly আর Worker মিলে এটা কীভাবে সম্ভব হলো?

একসময় ধরে নেওয়া হতো, ছবি সম্পাদনা বা ভেক্টর ডিজাইনের মতো ভারী কাজের জন্য ইনস্টল করা desktop অ্যাপ লাগবেই — JavaScript যথেষ্ট দ্রুত নয়, আর একটি thread-এ সব হয়। দুটি প্রযুক্তি এই ধারণা বদলেছে। প্রথমটি WebAssembly (২০১৭ সালে সব বড় ব্রাউজারে): C, C++ বা Rust-এ লেখা কোড compile করে ব্রাউজারে প্রায় native গতিতে চালানো যায়। দ্বিতীয়টি এই পর্বের Worker, আর তার সাথে SharedArrayBuffer — যা দিয়ে WebAssembly-র thread সম্ভব: Emscripten-এর মতো toolchain C/C++-এর pthread-কে Web Worker আর shared মেমরিতে রূপান্তর করে।

Figma শুরু থেকেই তাদের রেন্ডারিং ইঞ্জিন C++-এ লিখে ব্রাউজারে চালিয়েছে — প্রথমে asm.js হিসেবে, ২০১৭ সালে WebAssembly-তে; তাদের প্রকাশিত হিসাবে এতে লোড হওয়ার সময় তিন গুণেরও বেশি কমেছিল। Adobe ২০২১ সালে Photoshop-এর ওয়েব সংস্করণ ঘোষণা করে, যার বিশাল C++ কোডবেস WebAssembly-তে compile করা, আর multithreading-এর জন্য Worker ও SharedArrayBuffer ব্যবহার করে — ফলে পাতাটিকে cross-origin isolated হতে হয় (প্রশ্ন ২)। একই পথে চলেছে Google Earth, AutoCAD-এর ওয়েব সংস্করণ, ব্রাউজারে ভিডিও সম্পাদনার নানা টুল।

এখানে একটি সূক্ষ্ম কথা: WebAssembly নিজে main thread-এর সমস্যা মেটায় না। ২ সেকেন্ডের হিসাব WebAssembly-তে ৫০০ ms হলেও main thread-এ চললে পাতা ৫০০ ms জমে থাকে। তাই এই অ্যাপগুলোর নকশায় ভারী হিসাব প্রায় সবসময় Worker-এ, আর main thread শুধু UI আর canvas-এ ফল দেখানোয় — কখনো OffscreenCanvas দিয়ে সেটাও Worker-এ।

মূল উপলব্ধি: গতি আর সচলতা দুটি আলাদা সমস্যা — WebAssembly প্রথমটির উত্তর, Worker দ্বিতীয়টির। দুটি একসাথে হলেই ব্রাউজার একটি পূর্ণাঙ্গ অ্যাপ প্ল্যাটফর্ম হয়ে ওঠে।

অনুশীলন

প্রথমে নিজে ভাবুন বা চালিয়ে দেখুন, তারপর উত্তর মেলান।

  1. Worker দরকার কি না: নিচের প্রতিটি কাজের জন্য বলুন Worker ভালো সমাধান কি না, আর কেন — (ক) ১০,০০০ সারির একটি টেবিল DOM-এ আঁকা, (খ) একটি ৮ MB CSV ফাইল parse করে গড় বের করা, (গ) একটি API থেকে JSON আনা, (ঘ) একটি ৪০০ শব্দের ফর্মের লেখা যাচাই করা।

    (ক) না — ধীরতার কারণ DOM node বানানো আর layout, যা শুধু main thread পারে; সমাধান virtual list বা chunking. (খ) হ্যাঁ — CPU-ভারী, DOM লাগে না; File object Worker-এ পাঠিয়ে সেখানেই পড়ুন আর parse করুন, শুধু ফলাফল ফেরত দিন। (গ) না — নেটওয়ার্কের অপেক্ষা এমনিতেই main thread আটকায় না; তবে উত্তর বিশাল হলে তার প্রসেসিং Worker-এ নেওয়া যায়। (ঘ) না — কয়েক মিলিসেকেন্ডের কাজ; Worker-এ আসা-যাওয়ার খরচই বেশি হবে।

  2. আউটপুট অনুমান করুন: একটি ব্রাউজার script-এ লেখা আছে const buf = new ArrayBuffer(16); const view = new Uint8Array(buf); worker.postMessage(buf, [buf]); console.log(buf.byteLength, view.length); — কী ছাপা হবে? [buf] বাদ দিলে কী ছাপা হতো?

    ছাপা হবে 0 0. transfer করার পর buffer detached — তার byteLength ০, আর তার ওপর বানানো typed array-ও খালি হয়ে যায় (length ০)। [buf] বাদ দিলে buffer কপি হতো, main-এর হাতে আসলটা থাকত, আর ছাপা হতো 16 16. এটি নিজে যাচাই করতে অংশ ৩-এর চালানো যায় এমন উদাহরণে দুই লাইন যোগ করে দেখুন।

  3. বাগ খুঁজুন: একজন সহকর্মীর পণ্য খোঁজার কোডে প্রতিবার টাইপে লেখা const w = new Worker("search.js"); w.postMessage({ products, q }); w.onmessage = (e) => render(e.data);. ১,০০,০০০ পণ্যে টাইপ করার সময় পাতা আরও ধীর হয়ে যায়, আর মাঝে মাঝে পুরোনো ফল দেখায়। তিনটি সমস্যা চিহ্নিত করুন।

    (১) প্রতিটি অক্ষরে নতুন Worker — চালু হওয়ার খরচ বারবার, আর পুরোনো Worker-গুলো কখনো terminate হয় না, মেমরি জমতে থাকে। Worker একবার বানান। (২) প্রতিবার ১,০০,০০০ পণ্যের পুরো array কপি — এই structured clone নিজেই main thread-এ long task. পণ্যগুলো শুরুতে একবার পাঠিয়ে Worker-এ রেখে দিন, পরে শুধু q পাঠান। (৩) উত্তরের ক্রম নিশ্চিত নয় — “sh”-এর ধীর উত্তর “shi”-এর দ্রুত উত্তরের পরে এলে পুরোনো ফল বসে যায়। প্রতিটি অনুরোধে id দিন আর শুধু সর্বশেষ id-র উত্তর দেখান; সাথে debounce (পর্ব ৩৬)।

  4. কোড লিখুন (ঐচ্ছিক): অংশ ৫-এর চালানো যায় এমন RPC উদাহরণটি বদলে লিখুন, যাতে Worker অজানা type পেলে { id, ok: false, error } পাঠায়, আর callWorker তখন Promise reject করে। callWorker("unknown", 1).catch((e) => console.log("ভুল:", e.message)) দিয়ে পরীক্ষা করুন।

    Worker-এ: if (type !== "report" && type !== "search") return self.postMessage({ id, ok: false, error: "unknown type: " + type });, আর সফল উত্তরে { id, ok: true, result }. Main-এ Map-এ একটি object রাখুন: pending.set(id, { resolve, reject }). onmessage-এ const p = pending.get(id); pending.delete(id); data.ok ? p.resolve(data.result) : p.reject(new Error(data.error));. আউটপুটে আসবে ভুল: unknown type: unknown. Error object সরাসরিও পাঠানো যায় (structured clone আধুনিক ব্রাউজারে Error কপি করে), কিন্তু শুধু message string পাঠানো সবচেয়ে নিরাপদ ও সহজ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি Worker-এ কাজ পাঠিয়ে ফল দেখানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧵 Main thread new Worker() দিয়ে Worker তৈরি করে
  2. 📨 Main thread postMessage(data) ডাকে
  3. 📋 ব্রাউজার ডেটা structured clone দিয়ে কপি করে Worker-এ পৌঁছে দেয়
  4. ⚙️ Worker-এর onmessage চলে আর হিসাব করে
  5. 📬 Worker postMessage(result) দিয়ে ফল ফেরত পাঠায়
  6. 🎨 Main thread-এর onmessage ফল নিয়ে DOM বদলায়

একটি Web Worker-এর ভেতরে document.getElementById("total") লিখলে কী হয়?

  • Worker পাতার DOM-এর একটি কপি পায়, তাই element খুঁজে পায়
  • ReferenceError — Worker-এ document নেই
  • কাজটি স্বয়ংক্রিয়ভাবে main thread-এ চলে যায়
  • null ফেরত দেয়, কারণ element Worker-এর পাতায় নেই

ব্রাউজারে worker.postMessage(buf, [buf]) চালানোর ঠিক পরে main thread-এ buf.byteLength কত?

  • আগের মতোই — buffer কপি হয়েছে
  • TypeError ছোড়ে, কারণ buffer আর নেই
  • 0 — buffer detached হয়ে গেছে
  • অর্ধেক — দুই thread ভাগ করে নেয়

Web Worker-এ postMessage দিয়ে একটি function পাঠানোর চেষ্টা করলে কী হয়?

  • DataCloneError ছোড়ে
  • Function-এর source code string হয়ে পৌঁছায়
  • একই function-এর reference পৌঁছায়, দুই thread ভাগ করে ব্যবহার করে
  • Function চুপচাপ বাদ পড়ে, বাকি ডেটা পৌঁছায়

একই Worker-এ একসাথে কয়েকটি অনুরোধ পাঠিয়ে উত্তরগুলো ঠিক অনুরোধের সাথে মেলানোর সবচেয়ে নির্ভরযোগ্য উপায় কোনটি?

  • ধরে নেওয়া যে উত্তর পাঠানোর ক্রমেই আসবে
  • প্রতিটি অনুরোধের জন্য নতুন Worker তৈরি করা
  • প্রতিটি অনুরোধের মাঝে setTimeout দিয়ে দেরি করা
  • প্রতিটি বার্তায় অনন্য id দিয়ে, উত্তরের id অনুযায়ী Promise resolve করা

ব্রাউজারে SharedArrayBuffer ব্যবহার করতে পাতাকে কী শর্ত পূরণ করতে হয়?

  • Worker-কে অবশ্যই { type: "module" } দিয়ে তৈরি করতে হয়
  • পাতাকে COOP আর COEP header দিয়ে cross-origin isolated হতে হয়
  • ব্যবহারকারীর কাছে অনুমতির pop-up দেখাতে হয়
  • শুধু ব্রাউজারের একটি পরীক্ষামূলক flag চালু থাকলে

নিচের কোনটি service worker-এর কাজ, Web Worker-এর নয়?

  • ছবির pixel-এ grayscale ফিল্টার চালানো
  • ১,০০,০০০ পণ্যের তালিকায় খোঁজা
  • নেটওয়ার্ক request ধরে অফলাইনে cache থেকে উত্তর দেওয়া
  • আপলোডের আগে একটি বড় ফাইলের SHA-256 hash বানানো

একটি হিসাব ব্রাউজারে মাত্র ৩ ms নেয়। প্রতিবার ক্লিকে এর জন্য একটি নতুন Web Worker তৈরি করা কেমন সিদ্ধান্ত?

  • খারাপ — Worker চালু হওয়া আর বার্তার খরচ কাজের চেয়েই বেশি
  • ভালো — Worker-এ সব কাজ দ্রুত হয়
  • ভালো — না হলে INP খারাপ হবে
  • অসম্ভব — ১০০ ms-এর কম কাজে ব্রাউজার Worker চালায় না

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Main thread-এ চলে আপনার JavaScript, ইনপুট আর পর্দা আঁকা — একসাথে। ভারী হিসাব সেখানে মানে জমে যাওয়া পাতা। Worker একটি আলাদা thread, নিজের event loop আর global scope নিয়ে, কিন্তু DOM ছাড়া।
  2. যোগাযোগ শুধু বার্তায়: postMessage আর message event. ডেটা structured clone দিয়ে কপি হয় — function যায় না, বড় ডেটায় খরচ আছে।
  3. বড় বাইনারি ডেটা transfer করুন: postMessage(msg, [buf]) — কপি নেই, পাঠানো দিকের buffer detached (byteLength ০)।
  4. error event আর ভেতরে try/catch দিয়ে ভুল সামলান; কাজ শেষ বা বাতিল হলে terminate(). একাধিক অনুরোধে id + Promise (RPC), বা Comlink.
  5. Worker সচলতা দেয়, জাদু নয়: ছোট বা DOM-নির্ভর কাজে লাভ নেই। Node.js-এ একই চিন্তা worker_threads; service worker আর worklet আলাদা জিনিস।

পরের পর্বে: এতক্ষণ দেখলেন কোড কীভাবে দ্রুত আর সচল রাখা যায়। এবার কীভাবে নিরাপদ রাখা যায় — XSS কীভাবে ঢোকে, Content Security Policy কীভাবে ঠেকায়, আর CORS আসলে কী রক্ষা করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান