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

JavaScript কীভাবে চলে — engine, JIT আর call stack

How JavaScript Runs — Engines, JIT Compilation, the Call Stack, the Heap & the Event Loop

Taught in Bangla, this lesson follows JavaScript from source to machine code: the engine pipeline with parser, interpreter and JIT, hidden classes and inline caches, the call stack, the heap and garbage collection, the single thread and a first picture of the event loop, the language versus runtime split, and parse-time versus run-time errors with strict mode.

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

  • JavaScript engine কোডকে কোন কোন ধাপে CPU-র কাছে পৌঁছে দেয় — parsing, bytecode, JIT আর deoptimization
  • Object-এর “আকার” (hidden class) কেন গতির ওপর প্রভাব ফেলে
  • Call stack-এ frame কীভাবে জমে ও সরে, আর Maximum call stack size exceeded কেন আসে
  • Heap, reachability আর garbage collection — কোন object কখন মুছে যায়
  • Single thread, event loop-এর মূল ছবি, আর ভাষা (ECMAScript) বনাম runtime (ব্রাউজার, Node.js)-এর পার্থক্য

১ · আপনার কোড আর CPU-র মাঝখানে কে আছে

পর্ব ০৪-এ আপনি console.log("Hello") লিখে চালিয়েছেন, আর সাথে সাথে লেখাটি এসেছে। মনে হয় যেন কম্পিউটার JavaScript পড়েই বুঝে ফেলল। আসলে তা নয়। কম্পিউটারের CPU বোঝে শুধু নিজের machine code — খুব ছোট ছোট নির্দেশ, যেমন “এই দুটি সংখ্যা যোগ করো”, “এই ঠিকানা থেকে মান আনো”।

তাহলে মাঝখানে কে? একটি প্রোগ্রাম, যার নাম JavaScript engine. Engine আপনার লেখা পড়ে, বোঝে, আর CPU চালাতে পারে এমন রূপে বদলে দেয়। প্রতিটি বড় ব্রাউজারের নিজের engine আছে:

Engineকে বানায়কোথায় চলে
V8GoogleChrome, Edge, Opera, Node.js, Deno
SpiderMonkeyMozillaFirefox
JavaScriptCore (JSC)AppleSafari, iPhone-এর প্রায় সব ব্রাউজার, Bun

Microsoft Edge আগে নিজের engine (Chakra) ব্যবহার করত। ২০২০ সালের শুরুতে নতুন Edge Chromium-এর ওপর তৈরি হয়, তাই এখন সেটিও V8 চালায়। এই কোর্সে আমরা উদাহরণ হিসেবে মূলত V8 দেখব, কারণ আপনার Chrome, Edge আর Node.js — তিন জায়গাতেই একই engine. তবে মূল ধারণাগুলো তিনটি engine-এই প্রায় একই।

একটি বড় রেস্তোরাঁর রান্নাঘর ভাবুন। আপনি অর্ডার স্লিপে লিখলেন “এক প্লেট কাচ্চি”। রাঁধুনি সেই লেখা পড়েন (parsing), রান্নার ধাপ ঠিক করেন (bytecode), আর রান্না শুরু করেন। দুপুরে একই অর্ডার বারবার এলে অভিজ্ঞ রাঁধুনি আগে থেকেই উপকরণ মেপে সাজিয়ে রাখেন, যাতে পরের প্লেটগুলো অনেক দ্রুত বের হয় — এটাই JIT. JavaScript engine হলো সেই রান্নাঘর; আপনার কোড অর্ডার স্লিপ।

১৯৯৫ সালে Netscape-এ Brendan Eich মাত্র কয়েক দিনে JavaScript-এর প্রথম রূপ তৈরি করেন; সেই প্রথম engine থেকেই পরে SpiderMonkey এসেছে। শুরুর দিকের engine-গুলো ছিল সরল interpreter — কোড পড়ে পড়ে চালাত, তাই ধীর। ২০০৮ সালে সব বদলে যায়: Google Chrome প্রকাশ করে V8 (দলের নেতৃত্বে ছিলেন Lars Bak), যা কোডকে সরাসরি machine code-এ বানাত। প্রায় একই সময়ে Firefox আর Safari-ও নিজেদের JIT তৈরি করে। এই “গতির দৌড়” না হলে Google Docs, Figma বা আজকের ভারী ওয়েব অ্যাপ সম্ভব হতো না।

২ · Engine-এর ভেতরের পাইপলাইন — লেখা থেকে machine code

Engine একবারে সরাসরি machine code বানায় না। কয়েকটি ধাপে কাজ করে, আর প্রতিটি ধাপ পরের ধাপের জন্য জিনিস তৈরি করে দেয়। নিচের ডায়াগ্রামে “পরে” চাপুন — একটি ছোট function কীভাবে লেখা থেকে দ্রুত machine code হয়, আবার দরকার হলে কীভাবে পিছিয়ে আসে, দেখুন।

  1. Sourceapp.jsআপনার লেখা
  2. Scannertokensশব্দে ভাঙা
  3. ParserASTব্যাকরণের গাছ
  4. Ignitionbytecodeদ্রুত চালু
  5. Optimizermachine codeগরম কোডের জন্য
app.jsআপনি যা লিখেছেন
function area(r) {
    return 3.14 * r * r;
}
for (let i = 1; i <= 100000; i++) area(i);
area("5");   // শেষে একবার string!
Tokensঅক্ষরের সারি → অর্থপূর্ণ টুকরো
function  area  (  r  )  {
return  3.14  *  r  *  r  ;
}
keyword · নাম · চিহ্ন · সংখ্যা — space আর comment বাদ
ASTAbstract Syntax Tree (সরল রূপ)
FunctionDeclaration  area
├─ params: [ r ]
└─ body
   └─ ReturnStatement
      └─ BinaryExpression  *
         ├─ BinaryExpression  *
         │  ├─ Literal  3.14
         │  └─ Identifier  r
         └─ Identifier  r
BytecodeIgnition-এর নির্দেশ (সরল রূপ)
LdaConstant [3.14]   ; 3.14 হাতে নাও
Mul  r               ; r দিয়ে গুণ
Mul  r               ; আবার r দিয়ে গুণ
Return
; চলার সময় নোট রাখে: "r এ পর্যন্ত সবসময় number"
Machine codeঅনুমানের ওপর দাঁড়ানো দ্রুত কোড
check: r কি number?  না হলে → deopt
x = 3.14 × r         ; সরাসরি CPU-র গুণ
x = x × r
return x
; type খোঁজার কোনো ঝামেলা নেই — তাই বহু গুণ দ্রুত
Deoptimizationarea("5") — অনুমান ভেঙে গেল
check: r কি number?  → না, এটা string "5"
→ optimized কোড বাদ, Ignition-এর bytecode-এ ফেরা
→ bytecode নিয়ম মেনে চালায়: 3.14 * "5" * "5"
→ ফল 78.5   (ভুল ফল কখনো নয়, শুধু কিছুটা ধীর)
  1. ধাপ ১ — Source. আপনার ফাইল engine-এর কাছে শুধু অক্ষরের একটি লম্বা সারি। এখানে একটি function আছে যা ১ লক্ষবার ডাকা হবে, আর শেষে একবার ভুল করে string দিয়ে।
  2. ধাপ ২ — Scanner: token-এ ভাঙা। Scanner অক্ষরগুলোকে ছোট অর্থপূর্ণ টুকরো বা token-এ ভাঙে — এটা keyword, এটা নাম, এটা সংখ্যা, এটা চিহ্ন। বাংলা বাক্য পড়ার সময় আমরাও আগে শব্দ আলাদা করি, তারপর মানে বুঝি।
  3. ধাপ ৩ — Parser: ব্যাকরণের গাছ। Parser token থেকে একটি গাছ বানায় — AST. গাছটি বলে, “এখানে একটি function; তার ভেতরে একটি return; return করছে একটি গুণফল।” ব্যাকরণ ভুল হলে এখানেই SyntaxError — একটি লাইনও চলার আগে।
  4. ধাপ ৪ — Ignition: bytecode বানিয়ে চালানো। V8-এর interpreter Ignition গাছ থেকে bytecode বানায় এবং সাথে সাথে চালাতে শুরু করে। চালু হতে সময় কম লাগে, তাই পাতা দ্রুত সাড়া দেয়। চলার সময় সে চুপচাপ নোট রাখে — কোন function কতবার চলল, r-এ কোন type-এর মান এল।
  5. ধাপ ৫ — গরম কোড machine code-এ। area হাজার হাজার বার চলেছে, আর প্রতিবার r ছিল number. Engine বুঝল এটি “গরম” কোড। JIT compiler এবার ধরে নিল “r সবসময় number” — আর সেই অনুমানের ওপর সরাসরি CPU-র গুণের নির্দেশ দিয়ে machine code বানাল। শুরুতে শুধু একটি ছোট পাহারা বসানো থাকে: অনুমান ঠিক আছে কি না।
  6. ধাপ ৬ — Deoptimization. শেষ কলে এল string "5". পাহারা ধরে ফেলল অনুমান ভুল। Engine optimized কোড ফেলে দিয়ে আবার bytecode-এ ফিরে গেল — একে বলে deoptimization. JavaScript-এর নিয়ম মেনে "5" number-এ বদলে ফল এল 78.5. ফল কখনো ভুল হয় না; দাম শুধু কিছু সময়।
Source → Token → AST → Bytecode → (গরম হলে) Machine code
অনুমান ভাঙলে → আবার Bytecode

V8-এর কয়েক স্তরের compiler

উপরের ছবিতে “Optimizer” একটি বাক্স, কিন্তু সাম্প্রতিক V8 version-গুলোতে আসলে কয়েকটি স্তর আছে। প্রতিটি স্তর আগেরটির চেয়ে compile করতে বেশি সময় নেয়, কিন্তু বেশি দ্রুত কোড বানায়:

স্তরকী বানায়কখন কাজে লাগে
Ignition (interpreter)bytecode, তারপর চালায়সব কোড এখান থেকে শুরু
Sparkplug (baseline)bytecode থেকে খুব দ্রুত, সরল machine codeএকটু গরম কোড — প্রায় কোনো optimization নেই
Maglev (মাঝের স্তর)type-এর তথ্য দেখে মোটামুটি optimized কোডবেশ গরম কোড, দ্রুত compile দরকার
TurboFan (optimizing)সবচেয়ে গভীরভাবে optimized machine codeখুব গরম কোড — লম্বা সময় ধরে চলা লুপ, বারবার ডাকা function

Ignition আর TurboFan একসাথে চালু হয় ২০১৭ সালে, Sparkplug আসে ২০২১ সালে, আর Maglev ২০২৩ সালে। Engine-এর এই অংশ প্রায় প্রতি বছর বদলায় — তাই নামগুলো মুখস্থ করার চেয়ে মূল চিন্তাটা মনে রাখুন: শুরুতে দ্রুত চালু, পরে দ্রুত চলা।

Lazy parsing: একটি বড় ওয়েবসাইটে কয়েক মেগাবাইট JavaScript থাকতে পারে, যার অনেক function হয়তো কখনো ডাকাই হবে না। তাই engine শুরুতে প্রতিটি function পুরোপুরি parse করে না — শুধু দ্রুত একবার দেখে নেয় (pre-parse) ব্যাকরণ ঠিক আছে কি না আর function কোথায় শেষ। পুরো parse আর bytecode তৈরি হয় যখন function-টি প্রথমবার ডাকা হয়। এজন্য পাতা দ্রুত খোলে।

৩ · Hidden class আর inline cache — object-এর “আকার” কেন গুরুত্বপূর্ণ

JavaScript-এ object-এ যখন খুশি নতুন property যোগ করা যায়, মুছেও ফেলা যায়। এটি লেখার জন্য আরামদায়ক, কিন্তু engine-এর জন্য কঠিন। C বা Java-তে compiler আগেই জানে একটি object-এর ভেতরে কোন field কোথায় আছে। JavaScript-এ p.x পড়তে গেলে engine-কে প্রতিবার খুঁজতে হতো “x কোথায় আছে?” — অভিধানে শব্দ খোঁজার মতো, যা ধীর।

সমাধান হলো hidden class (V8-এর ভেতরে এর নাম “Map”, অন্য engine-এ “Shape” বা “Structure”). একই property একই ক্রমে আছে এমন সব object একই hidden class ভাগ করে নেয়। Hidden class লিখে রাখে: “x আছে ঘর ০-এ, y আছে ঘর ১-এ”। তখন p.x মানে সরাসরি ঘর ০ — খোঁজার দরকার নেই।

// একই আকার: একই property, একই ক্রমে
const a = { x: 1, y: 2 };
const b = { x: 5, y: 9 };          // a আর b একই hidden class

// আকার আলাদা হয়ে যায় এমন কাজ
const c = { y: 2, x: 1 };          // ক্রম উল্টো → নতুন hidden class
const d = { x: 1 };  d.y = 2;      // পরে যোগ → hidden class বদল (transition)
d.z = 3;                           // আরেকটি বদল
delete a.x;                        // delete → প্রায়ই ধীর "অভিধান" রূপে চলে যায়

এই কোড চালানোর জন্য নয় — শুধু আকারের পার্থক্য দেখানোর জন্য। a আর b একই আকারের, তাই engine একটি hidden class দিয়েই দুটিকে চেনে। c-তে একই দুটি property, কিন্তু ক্রম উল্টো — engine-এর চোখে এটি ভিন্ন আকার। d-তে property একটির পর একটি যোগ হওয়ায় প্রতিবার নতুন hidden class-এ যেতে হয়। আর delete অনেক engine-এ object-কে ধীর “অভিধান রূপে” পাঠিয়ে দেয়।

এবার এর সাথে যোগ করুন inline cache. ধরুন একটি function বারবার p.x + p.y পড়ে। প্রথমবার engine খুঁজে বের করে x কোথায়, তারপর সেই জায়গায় লিখে রাখে: “গতবার hidden class ছিল H1, x ছিল ঘর ০-এ”। পরের বার একই hidden class এলে খোঁজা ছাড়াই সরাসরি ঘর ০। একটি লুপে কী ঘটে, দেখুন:

লুপে যে object আসেInline cache-এর অবস্থাফল
সবগুলো একই আকারের {x, y}Monomorphic — একটিই আকার মনে রাখেসবচেয়ে দ্রুত; optimizer সহজে machine code বানায়
২–৪ রকম আকার মিশ্রিতPolymorphic — কয়েকটি আকারের তালিকাপ্রতিবার তালিকা মেলাতে হয়, একটু ধীর
অনেক রকম আকার (কোনোটায় আগে y, কোনোটায় বাড়তি z…)Megamorphic — মনে রাখা ছেড়ে দেয়প্রতিবার সাধারণ খোঁজা — অনেক ধীর
মূল ধারণা

একই কাজের object-গুলো একই জায়গায়, একই ক্রমে, একই property দিয়ে তৈরি করুন — যেমন একটি constructor বা factory function দিয়ে। আর একই property-তে একই type রাখুন (কখনো number, কখনো string নয়)। Engine তখন অনুমান করতে পারে, আর অনুমান টিকে থাকলেই কোড দ্রুত থাকে। অধিকাংশ কোডে এটি স্বাভাবিকভাবেই হয় — এর জন্য বাড়তি কষ্ট করার দরকার নেই।

এটি জানার উদ্দেশ্য “অপ্টিমাইজেশনের কৌশল” মুখস্থ করা নয়। এটি বোঝায় কেন JavaScript-এর গতি অনুমাননির্ভর, আর কেন একই কোড কখনো দ্রুত, কখনো ধীর হতে পারে। সত্যিকারের ধীরগতি খুঁজতে হয় মাপজোখ করে — পর্ব ৫৩-এ DevTools-এর profiler দিয়ে সেটি করবেন।

৪ · Call stack — কে কাকে ডাকল, তার হিসাবের স্তূপ

এবার কোড চলার সময় engine কীভাবে মনে রাখে “এখন কোন function চলছে, শেষ হলে কোথায় ফিরতে হবে”। এর জন্য আছে call stack — বিয়েবাড়িতে একটার ওপর একটা রাখা প্লেটের স্তূপের মতো। নতুন প্লেট রাখা হয় ওপরে, তোলাও হয় ওপর থেকে।

যখনই একটি function ডাকা হয়, engine তার জন্য একটি execution context তৈরি করে — সেই function-এর local variable, argument, this আর “শেষ হলে কোথায় ফিরব” এর ঠিকানা। এই তথ্যের প্যাকেটটি একটি stack frame হিসেবে স্তূপের ওপরে বসে। Function শেষ হলে frame সরে যায়। সবার নিচে থাকে global context — পুরো script-এর জন্য।

1function format(name) {2    return "Hello, " + name;3}4function greet(name) {5    const msg = format(name);6    console.log(msg);7}8greet("Rafi");
Call stack (ওপরেরটি এখন চলছে)
stack
msg
  1. Global context. Script শুরু হলো। Stack-এ শুধু global frame. লাইন ৮-এ greet("Rafi") ডাকা হবে।
  2. greet-এর frame push. greet ডাকা হলো — তার নতুন frame বসল global-এর ওপরে। এই frame-এ আছে name = "Rafi" আর msg-এর ঘর (এখনো মান নেই), আর লেখা আছে শেষ হলে লাইন ৮-এ ফিরতে হবে।
  3. format-এর frame push. লাইন ৫-এ greet ডাকল format-কে। greet থেমে অপেক্ষা করছে; তার ওপরে বসল format-এর frame. এখন stack-এ তিনটি স্তর।
  4. format return করল — pop. "Hello, Rafi" তৈরি করে ফেরত দিল। format-এর frame সরে গেল, তার local সব কিছু হারিয়ে গেল। ফেরত মানটি greet-এর msg-এ বসল।
  5. console.log-ও একটি function. লাইন ৬-এ console.log ডাকা হলো — তারও একটি frame সাময়িকভাবে stack-এর ওপরে বসল।
  6. লেখা এল, console.log pop. আউটপুটে Hello, Rafi. console.log-এর frame সরে গেল; আবার ওপরে greet.
  7. greet শেষ — pop. লাইন ৭-এ greet-এর কাজ শেষ। Frame সরে গেল, msg-ও আর নেই। নিয়ন্ত্রণ ফিরল লাইন ৮-এ।
  8. Script শেষ — stack খালি। Global কোড শেষ, call stack খালি। এই “খালি stack” মুহূর্তটি গুরুত্বপূর্ণ — event loop ঠিক এই সময়ের অপেক্ষায় থাকে (অংশ ৬)।

Error হলে আপনি যে stack trace দেখেন, সেটি আসলে এই স্তূপের একটি ছবি — error-এর মুহূর্তে কোন function কাকে ডেকেছিল, ওপর থেকে নিচে। পর্ব ২৯-এ error নিয়ে বিস্তারিত দেখবেন।

Stack ভরে গেলে — RangeError

প্লেটের স্তূপ অসীম উঁচু হতে পারে না। Call stack-এরও একটি সীমা আছে। একটি function যদি নিজেকে ডাকে (recursion) কিন্তু কখনো থামার শর্ত না থাকে, frame জমতেই থাকে — শেষে engine হাল ছেড়ে দেয়। চালিয়ে দেখুন:

let depth = 0;

function dive() {
  depth++;        // কতগুলো frame জমল গুনছি
  dive();         // থামার কোনো শর্ত নেই!
}

try {
  dive();
} catch (e) {
  console.log(e.name);
  console.log(e.message);
  console.log("হাজারের বেশি frame জমেছিল?", depth > 1000);
}

// থামার শর্ত থাকলে কোনো সমস্যা নেই
function countdown(n) {
  return n === 0 ? "শেষ!" : countdown(n - 1);
}
console.log(countdown(100));

আউটপুটে আসে RangeError, তারপর Maximum call stack size exceeded — V8-এর বার্তা, তাই Chrome, Edge আর Node.js-এ একই। (Firefox-এ বার্তাটি ভিন্ন: too much recursion.) ঠিক কতগুলো frame জমার পর থামে, সেই সংখ্যা ব্রাউজার, version আর প্রতিটি frame-এর আকারের ওপর নির্ভর করে — তাই আমরা শুধু ছেপেছি “হাজারের বেশি কি না”, যা true. শেষ লাইনের countdown(100) ১০১টি frame জমিয়ে আবার একে একে সরিয়ে দেয়, ছাপে শেষ! — recursion নিজে খারাপ নয়, থামার শর্ত না থাকাটাই বাগ। খুব গভীর ডেটায় (যেমন লাখ স্তরের নেস্টেড তালিকা) থামার শর্ত থাকলেও সীমা ছাড়াতে পারে; তখন লুপ ব্যবহার করুন।

৫ · Heap আর garbage collection — কে মেমরি পরিষ্কার করে

Call stack ছোট আর গোছানো — frame আসে, frame যায়। কিন্তু object, array, function-এর মতো বড় বা দীর্ঘজীবী জিনিস থাকে আরেক জায়গায়: heap. Heap একটি বড় গুদামঘরের মতো; stack-এর variable-এ থাকে শুধু গুদামের সেই তাকের ঠিকানা (reference).

C-তে malloc দিয়ে নেওয়া মেমরি আপনাকে নিজেই free করতে হয়। JavaScript-এ সেই দায়িত্ব engine-এর — garbage collector (GC). প্রশ্ন হলো, GC কীভাবে জানে কোন object আর দরকার নেই? উত্তর: reachability. কিছু সূচনাবিন্দু আছে, যাদের বলে root — global object, call stack-এ থাকা সব local variable. Root থেকে reference ধরে ধরে যে object-এ পৌঁছানো যায়, সেটি “জীবিত”। যেটিতে কোনোভাবেই পৌঁছানো যায় না, সেটি আবর্জনা।

Heap-এর object (root: global app, stack-এ user)
A · app.config
B · user object
C · পুরোনো cart
D · cart.items (C↔D একে অপরকে দেখায়)
  1. শুরুর অবস্থা। Heap-এ চারটি object. A-কে দেখায় global app; B-কে দেখায় একটি চলমান function-এর local variable user. C আর D একসময় ব্যবহার হতো — এখন কোনো variable তাদের দেখায় না, কিন্তু C আর D একে অপরকে দেখায় (একটি চক্র)।
  2. Mark: global root থেকে। GC global root থেকে শুরু করে reference ধরে এগোয়। app → A. A-তে চিহ্ন পড়ল: জীবিত।
  3. Mark: stack root থেকে। এবার call stack-এর local variable. user → B. B-তেও চিহ্ন। আর কোনো root নেই।
  4. চক্র হলেও লাভ নেই। C আর D একে অপরকে দেখালেও কোনো root থেকে তাদের কাছে পৌঁছানোর পথ নেই। তাই তারা অচিহ্নিত রইল। (শুধু “কতজন আমাকে দেখায়” গুনে মুছলে — reference counting — এই চক্র কখনো মুছত না। Mark-and-sweep এই সমস্যা নিজে থেকেই এড়ায়।)
  5. Sweep: অচিহ্নিত সব মুছে ফেলা। GC heap ঘুরে অচিহ্নিত C আর D-এর জায়গা খালি করে দিল। এটাই mark-and-sweep.
  6. Function শেষ হলে। যে function-এ user ছিল, সেটি return করলে তার frame সরে যায় — B-তে পৌঁছানোর আর কোনো পথ থাকে না। পরের বার GC চললে B-ও মুছবে। কখন চলবে, তা engine ঠিক করে; আপনি নিয়ন্ত্রণ করেন না।

Orinoco — V8 কীভাবে GC-কে চোখের আড়ালে রাখে

পুরোনো GC চলার সময় পুরো প্রোগ্রাম থামিয়ে দিত (“stop-the-world”). একটি অ্যানিমেশন বা গেমে ১০০ মিলিসেকেন্ডের থামা মানে চোখে পড়ার মতো ঝাঁকুনি। V8-এর GC প্রজেক্ট Orinoco কয়েকটি কৌশলে এই থামা অনেক কমিয়েছে:

  • Generational: বেশিরভাগ object খুব অল্প সময় বাঁচে (একটি লুপের ভেতরের অস্থায়ী object). তাই heap দুই ভাগে — নতুন object-এর ছোট “young generation”, যা ঘনঘন ও দ্রুত পরিষ্কার হয়; আর কয়েকবার টিকে যাওয়া object চলে যায় বড় “old generation”-এ, যা কম পরিষ্কার হয়।
  • Parallel: GC-র কাজ কয়েকটি helper thread-এ ভাগ করে করা, যাতে থামার সময় কম হয়।
  • Concurrent ও incremental: চিহ্ন দেওয়ার বড় অংশ আপনার কোড চলার সাথে সাথে পেছনের thread-এ করা, বা ছোট ছোট টুকরোয় ভাগ করা — যাতে একবারে লম্বা থামা না হয়।

GC আছে মানে memory leak হয় না — এই ধারণা ভুল। GC শুধু পৌঁছানো যায় না এমন object মোছে, অপ্রয়োজনীয় object নয়। কোনো global array-তে জিনিস যোগ করেই চললে, বা বন্ধ হয়ে যাওয়া পাতার অংশের সাথে event listener লাগিয়ে রাখলে সেগুলো সবসময় reachable — GC ছোঁয় না, মেমরি বাড়তেই থাকে। এটাই memory leak. DevTools দিয়ে leak খোঁজা শিখবেন পর্ব ৫৩-এ।

৬ · একটিই thread — পাতা কেন জমে যায়, আর event loop-এর ছবি

আপনার JavaScript কোড চলে একটি thread-এ — অর্থাৎ একটিমাত্র call stack, একসময়ে একটিমাত্র কাজ। ব্রাউজারে এই thread-কে বলে main thread, আর এই একই thread পাতা আঁকা, বোতামের ক্লিক সামলানো, scroll — সবকিছুর দায়িত্বেও থাকে। একে বলে single-threaded execution.

এর ফল কী, ভাবুন। নিচের কোডটি কোনো বোতামের ক্লিকে চললে কী হবে?

button.addEventListener("click", () => {
  let total = 0;
  for (let i = 0; i < 3_000_000_000; i++) {   // কয়েকশো কোটি বার
    total += i;
  }
  result.textContent = total;
});

এটি এখানে চালানোর জন্য দেওয়া হয়নি — চালালে পাতাটি কয়েক সেকেন্ড জমে থাকবে। লুপ চলাকালীন call stack কখনো খালি হয় না। ফলে ব্রাউজার পাতা নতুন করে আঁকতে পারে না, অন্য বোতাম কাজ করে না, scroll আটকে যায়; বেশিক্ষণ চললে ব্রাউজার “Page unresponsive” জাতীয় সতর্কবার্তা দেখায়। ঢাকার এক লেনের রাস্তায় একটি ট্রাক থেমে থাকলে যেমন পেছনের সব রিকশা আটকে যায় — তেমন। সমাধান: ভারী কাজ টুকরো করা, বা আলাদা thread-এ পাঠানো — Web Worker (পর্ব ৪২)।

তাহলে setTimeout বা নেটওয়ার্ক থেকে ডেটা আনার মতো “অপেক্ষার” কাজ কীভাবে হয়, যদি thread একটিই? উত্তর: অপেক্ষার কাজটা JavaScript engine করেই না। Timer, নেটওয়ার্ক, ক্লিক শোনার কাজ করে ব্রাউজার (বা Node.js) নিজে — এদের বলে Web API. কাজ শেষ হলে আপনার callback একটি লাইনে (queue) অপেক্ষা করে। Call stack খালি হলেই event loop সেই লাইন থেকে পরের কাজটি stack-এ তোলে। লাইন দুই রকম: সাধারণ task queue (setTimeout, ক্লিক) আর অগ্রাধিকারের microtask queue (Promise-এর then). নিচের ছবিটি শুধু মূল ধারণা — বিস্তারিত পর্ব ৩১-এ।

1console.log("1: শুরু");2setTimeout(() => console.log("4: timeout"), 0);3Promise.resolve().then(() => console.log("3: promise"));4console.log("2: শেষ");
কে কোথায় অপেক্ষা করছে
Call stack
Web API (timer)
Microtask queue
Task queue
  1. Sync কোড চলছে। লাইন ১ সরাসরি call stack-এ চলল, লেখা এল 1: শুরু.
  2. setTimeout — কাজটা ব্রাউজারকে দেওয়া। setTimeout নিজে অপেক্ষা করে না। সে ব্রাউজারের timer-কে বলে, “০ ms পরে এই callback-টা লাইনে দিও” — এবং সাথে সাথে ফিরে আসে। JavaScript পরের লাইনে চলে যায়।
  3. দুই লাইনে দুই callback. Timer-এর সময় শেষ, তাই timeout callback দাঁড়াল task queue-তে। লাইন ৩-এ Promise আগেই resolved, তাই তার then callback দাঁড়াল microtask queue-তে। কিন্তু কোনোটাই এখন চলতে পারবে না — call stack এখনো ব্যস্ত।
  4. Sync কোডের শেষ লাইন। লাইন ৪ চলল, এল 2: শেষ. খেয়াল করুন — “০ ms” timer থাকা সত্ত্বেও এটি আগে এল। ০ মানে “এখনই” নয়, মানে “যত তাড়াতাড়ি সম্ভব, কিন্তু চলমান কোড শেষ হলে”।
  5. Stack খালি → আগে সব microtask. Script শেষ, call stack খালি। Event loop প্রথমে microtask queue পুরোপুরি খালি করে — promise callback চলল, এল 3: promise.
  6. তারপর একটি task. Microtask শেষ হলে event loop task queue থেকে একটি কাজ তোলে — timeout callback চলল, এল 4: timeout. (দুই task-এর মাঝখানে ব্রাউজার দরকার হলে পাতা নতুন করে আঁকে।)

নিজে চালিয়ে মিলিয়ে নিন — এখানে একটি বাড়তি then-ও আছে:

console.log("A: script শুরু");

setTimeout(() => console.log("E: setTimeout 0"), 0);

Promise.resolve()
  .then(() => console.log("C: প্রথম then"))
  .then(() => console.log("D: দ্বিতীয় then"));

console.log("B: script শেষ");

আউটপুট সবসময় এই ক্রমে: A, B, C, D, E. প্রথমে দুটি sync লাইন (A, B). তারপর stack খালি হলে microtask: প্রথম then চলল (C), আর সেটি শেষ হতেই দ্বিতীয় then-এর callback নতুন microtask হিসেবে লাইনে ঢুকল — event loop microtask queue পুরো খালি না হওয়া পর্যন্ত চালায়, তাই D-ও timeout-এর আগে। সবশেষে task queue থেকে setTimeout-এর callback (E). কোডে setTimeout আগে লেখা হলেও চলেছে সবার শেষে।

৭ · ভাষা বনাম runtime — ECMAScript, ব্রাউজার আর Node.js

খেয়াল করেছেন, setTimeout বা document engine-এর অংশ নয় বললাম? এখানে একটি গুরুত্বপূর্ণ পার্থক্য আছে। JavaScript ভাষার নিয়ম লেখা আছে একটি মানদণ্ডে, যার নাম ECMAScript (Ecma International-এর TC39 কমিটি প্রতি বছর নতুন version প্রকাশ করে — ES2015, ES2016 … )। এতে আছে variable, function, object, Promise, Array-এর মতো মূল জিনিস। কিন্তু “পাতায় লেখো”, “ফাইল পড়ো”, “৩ সেকেন্ড পরে চালাও” — এগুলো ভাষার অংশ নয়।

এসব যোগ করে runtime বা host environment — engine-কে ঘিরে থাকা পরিবেশ, যা বাড়তি ক্ষমতা দেয়:

স্তরকী দেয়উদাহরণ
ECMAScript (ভাষা, engine বাস্তবায়ন করে)মূল ব্যাকরণ ও built-in objectlet, class, Array, Map, Promise, JSON
ব্রাউজার runtimeপাতা, নেটওয়ার্ক, timer, storagedocument (DOM), fetch, setTimeout, localStorage
Node.js runtimeফাইল, process, সার্ভারfs, process, http, Buffer

এজন্যই Node.js-এ document লিখলে ReferenceError: document is not defined আসে, আর ব্রাউজারে require("fs") কাজ করে না। দুই জায়গাতেই engine একই (V8), ভাষাও একই — শুধু চারপাশের পরিবেশ আলাদা। console আর setTimeout দুই runtime-ই দেয়, তাই দুই জায়গায় চলে। Node.js-এর সাথে Deno আর Bun-ও এখন জনপ্রিয় runtime — প্রথম ভাবনার প্রশ্নে এ নিয়ে আরও আছে।

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

Java-র সাথে মিল সবচেয়ে বেশি: JVM-ও আগে interpreter-এ চালায়, গরম কোড JIT দিয়ে compile করে, অনুমান ভাঙলে deoptimize করে, আর GC-ও generational (Java কীভাবে চলে)। পার্থক্য হলো, Java-তে javac আগেই bytecode বানিয়ে রাখে আর type জানা থাকে; JavaScript-এ source পাঠানো হয়, bytecode তৈরি হয় চলার সময়, আর type অনুমান করতে হয় দেখে দেখে। CPython-ও bytecode বানায়, কিন্তু মেমরি মূলত reference counting দিয়ে সামলায় (Python কীভাবে চলে)। C/C++-এ আগেই machine code তৈরি হয়, আর stack/heap একই ধারণা — শুধু heap নিজে হাতে মুক্ত করতে হয়।

৮ · ভুল কখন ধরা পড়ে — parse time বনাম run time, আর strict mode

পাইপলাইন বোঝার একটি বড় লাভ: error দেখে বলতে পারবেন সেটি কোন ধাপের। JavaScript-এ ভুল মূলত দুই সময়ে ধরা পড়ে।

🌳 Parse time — চলার আগে

ব্যাকরণের ভুল (SyntaxError) parser ধরে, কোনো লাইন চলার আগেই। ফলে পুরো script-এর একটি লাইনও চলে না — ভুলের আগের লাইনগুলোও নয়।

যেমন: বন্ধনী খোলা রাখা, let x = ;

⚙️ Run time — চলার সময়

ব্যাকরণ ঠিক, কিন্তু চলতে গিয়ে সমস্যা: নেই এমন variable (ReferenceError), undefined-এর property পড়া (TypeError), stack ভরে যাওয়া (RangeError). ভুলের আগের লাইনগুলো কিন্তু চলে যায়।

যেমন: user.name যখন user = undefined
console.log("a");
let x = ;

Node.js-এ এই ফাইল চালালে a ছাপা হয় না — সরাসরি আসে নিচের বার্তা। Chrome বা Edge-এর console-এ একই বার্তার শুরুতে Uncaught যোগ হয়। প্রথম লাইনে কোনো ভুল না থাকলেও সেটি চলেনি, কারণ parser পুরো ফাইল না বুঝে কিছুই চালায় না।

SyntaxError: Unexpected token ';'

একটি সূক্ষ্ম বিষয়: ব্রাউজারে প্রতিটি <script> ট্যাগ আলাদাভাবে parse হয়। তাই একটি ফাইলে SyntaxError থাকলে শুধু সেই script বাদ পড়ে; অন্য script ঠিকই চলে — আর তারা যদি ভাঙা script-এর function খোঁজে, পাবে run-time-এর ReferenceError.

Strict mode — ভুলকে চুপচাপ মেনে না নেওয়া

শুরুর দিকের JavaScript অনেক ভুল চুপচাপ মেনে নিত। ২০০৯ সালের ES5 যোগ করে strict mode — ফাইল বা function-এর শুরুতে "use strict"; লিখলে engine কড়া নিয়মে চলে। কয়েকটি প্রভাব:

  • ঘোষণা না করা variable-এ মান বসালে (score = 10) চুপচাপ global variable তৈরি হয় না — আসে ReferenceError: score is not defined.
  • সাধারণ function কলে this হয় undefined, global object নয় (পর্ব ২৪)।
  • একই নামের দুটি parameter, বা with statement — parse time-এই SyntaxError.
  • শুধু-পড়া যায় এমন property-তে লিখলে নীরবে ব্যর্থ না হয়ে TypeError.

সুখবর: ES module (import/export, পর্ব ৪৫) আর class-এর ভেতরের কোড নিজে থেকেই strict. তাই আধুনিক প্রজেক্টে আলাদা করে লেখার দরকার খুব কম হয়।

পরীক্ষা ও ইন্টারভিউতে
  • JavaScript কি interpreted না compiled? — দুটোই: engine প্রথমে bytecode বানিয়ে interpreter-এ চালায়, আর গরম কোড JIT দিয়ে চলার সময় machine code-এ compile করে।
  • Call stack কী? — চলমান function-গুলোর frame-এর স্তূপ (LIFO); ডাকলে push, return করলে pop. সীমা ছাড়ালে RangeError: Maximum call stack size exceeded.
  • JavaScript single-threaded হলে asynchronous কাজ কীভাবে হয়? — অপেক্ষার কাজ করে runtime (Web API বা Node.js); callback queue-তে দাঁড়ায়, আর call stack খালি হলে event loop সেটি চালায় — আগে microtask, পরে task.
  • Garbage collector কোন object মোছে? — root (global, stack-এর variable) থেকে reference ধরে যেটিতে পৌঁছানো যায় না; পরস্পরকে দেখানো চক্রও মোছে (mark-and-sweep).
  • V8 কী? — Google-এর open-source JavaScript ও WebAssembly engine, Chrome, Edge আর Node.js-এ ব্যবহৃত; ভাষার মানদণ্ড ECMAScript, আর DOM বা fs দেয় runtime.

৯ · AI-এর যুগে — পারফরম্যান্সের পুরোনো গল্প বনাম মাপজোখ

“JavaScript দ্রুত করার ১০টি টিপস” — এমন লেখা ইন্টারনেটে অগণিত, আর AI সেগুলো থেকেই শিখেছে। সমস্যা হলো, এর অনেকগুলো লেখা হয়েছিল ২০১০-এর আগের engine-এর জন্য, যখন JIT ছিল না বা খুব সরল ছিল। আজকের engine সেই সব কৌশল নিজেই করে ফেলে, আর কিছু কৌশল উল্টো ক্ষতি করে — কোড কঠিন হয়, কিন্তু গতি বাড়ে না।

AI বা পুরোনো ব্লগ যা বলেআধুনিক engine-এ বাস্তবতা
“লুপে arr.length আগে একটি variable-এ রাখুন”Optimizer সাধারণত এটা নিজেই করে; পার্থক্য প্রায় নেই। পড়ার সুবিধাই বড়।
“for লুপ সবসময় map/forEach-এর চেয়ে অনেক দ্রুত”খুব গরম লুপে সামান্য পার্থক্য হতে পারে; বেশিরভাগ কোডে চোখে পড়ার মতো নয়। মেপে দেখুন।
“মেমরি বাঁচাতে কাজ শেষে variable-এ null দিন”Local variable function শেষ হলেই unreachable হয়। শুধু দীর্ঘজীবী reference (global, cache) ছাড়ার সময় কাজের।
“++i লিখুন, i++-এর চেয়ে দ্রুত”ফল ব্যবহার না হলে engine-এর কাছে দুটো একই।
“একবার console.time দিয়ে মেপে দেখলাম, এটাই দ্রুত”JIT warm-up, GC আর tier বদলের কারণে একবারের মাপ বিভ্রান্তিকর। বাস্তব অবস্থায় profiler দিয়ে মাপুন।
ধীরগতির কারণ অনুমান করবেন না — মাপবেন।
Profiler যা দেখায়, সেটাই সত্য।

AI-কে পারফরম্যান্স নিয়ে প্রশ্ন করলে তাকে কাঠামোর মধ্যে রাখুন — কোন engine, কী মাপা হয়েছে, আর প্রতিটি দাবির কারণ:

আমার JavaScript কোড Chrome-এ ধীর মনে হচ্ছে। কোডটি নিচে দিলাম: [এখানে কোড পেস্ট করুন] ১. সম্ভাব্য ধীরগতির কারণ বলো, কিন্তু প্রতিটি কারণের পাশে লিখে দাও এটা আধুনিক V8-এ এখনো সত্যি কি না, আর কেন। ২. পুরোনো "micro-optimization" টিপস দিও না — যদি দাও, স্পষ্ট লিখবে সেটা পুরোনো engine-এর জন্য ছিল। ৩. Chrome DevTools-এর Performance প্যানেলে আমি কীভাবে মেপে নিশ্চিত হব, ধাপে ধাপে বলো। ৪. কোনো পরিবর্তন প্রস্তাব করলে আগে-পরে কী মাপলে উন্নতি প্রমাণ হবে, সেটাও বলো।

AI-এর পারফরম্যান্স পরামর্শ হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১“JavaScript engine” আর “runtime” কি একই জিনিস? Node.js, Deno আর Bun কেন আলাদা engine বেছে নিয়েছে?

না, একই নয়। Engine শুধু ভাষাটি চালায় — parse, bytecode, JIT, GC. Runtime হলো engine-এর চারপাশে তৈরি পুরো পরিবেশ: ফাইল পড়া, নেটওয়ার্ক, timer, event loop, module লোড করা, package-এর ব্যবস্থা। একটি গাড়ির কথা ভাবুন — engine গাড়িকে চালায়, কিন্তু চাকা, স্টিয়ারিং, সিট ছাড়া সেটি গাড়ি নয়।

  • Node.js (২০০৯): Ryan Dahl V8 নিয়ে তার চারপাশে C/C++-এ একটি runtime বানান, সাথে libuv নামের library, যা ফাইল ও নেটওয়ার্কের অপেক্ষার কাজ সামলায়। মূল চিন্তা ছিল: ব্রাউজারের মতোই একটি thread আর event loop দিয়ে হাজার হাজার সংযোগ সামলানো। সেই সময় V8 ছিল সবচেয়ে দ্রুত open-source engine, তাই সিদ্ধান্তটি স্বাভাবিক ছিল।
  • Deno: ২০১৮ সালে একটি বক্তৃতায় Ryan Dahl নিজেই Node.js-এর নকশার কিছু ভুলের কথা বলেন — যেমন নিরাপত্তার অনুমতি না থাকা আর module ব্যবস্থার জটিলতা। এরপর তিনি Deno শুরু করেন: engine সেই V8-ই, কিন্তু runtime লেখা Rust-এ, ফাইল বা নেটওয়ার্ক ব্যবহার করতে স্পষ্ট অনুমতি লাগে, TypeScript সরাসরি চলে।
  • Bun: Jarred Sumner-এর তৈরি, ২০২৩ সালে version 1.0. এটি Zig ভাষায় লেখা এবং engine হিসেবে বেছে নিয়েছে Apple-এর JavaScriptCore. এর পেছনে মূল যুক্তি ছিল দ্রুত চালু হওয়া — JSC শুরুতে দ্রুত সাড়া দেয় বলে পরিচিত। সাথে package manager, bundler, test runner — সব একসাথে।

খেয়াল করুন, তিনটির ভাষা একই — ECMAScript. পার্থক্য engine-এর বৈশিষ্ট্যে (চালু হওয়ার গতি, দীর্ঘ সময়ের গতি, মেমরি) আর runtime-এর নকশায় (নিরাপত্তা, টুল, Node.js-এর সাথে সামঞ্জস্য)। এজন্যই কোনো কোড এক runtime-এ চলে, আরেকটিতে চলে না — সাধারণত ভাষার কারণে নয়, runtime-এর API-র কারণে।

মূল উপলব্ধি: ভাষা, engine আর runtime — তিনটি আলাদা স্তর। কোনো সমস্যা হলে আগে ঠিক করুন সেটি কোন স্তরের; তাহলেই বুঝবেন ডকুমেন্টেশন কোথায় খুঁজবেন — ECMAScript-এ, MDN-এর Web API অংশে, না Node.js-এর ডকে।

প্র ০২২০০৮ সালে V8 আসার পর ব্রাউজারগুলোর মধ্যে “গতির দৌড়” কেন শুরু হলো, আর তা ওয়েবকে কীভাবে বদলে দিল?

২০০৮ সালের আগে JavaScript-কে অনেকে “খেলনা ভাষা” ভাবতেন — ফর্ম যাচাই বা ছোট অ্যানিমেশনের জন্য। Engine-গুলো ছিল সরল interpreter. কিন্তু Gmail (২০০৪) আর Google Maps (২০০৫) দেখিয়ে দিয়েছিল যে ব্রাউজারেই পূর্ণ অ্যাপ বানানো সম্ভব — শুধু engine যদি দ্রুত হয়।

Google তখন ডেনমার্কে Lars Bak-এর নেতৃত্বে একটি দল দিয়ে V8 বানায়। Bak আগে Java-র HotSpot VM আর Smalltalk-এর দ্রুত VM নিয়ে কাজ করেছিলেন — hidden class আর inline cache-এর ধারণা সেই Smalltalk/Self গবেষণা থেকেই এসেছে। ২০০৮ সালের সেপ্টেম্বরে Chrome-এর সাথে V8 প্রকাশ পায়, আর সেটি শুরু থেকেই কোডকে machine code-এ compile করত। কয়েকটি benchmark-এ গতির পার্থক্য ছিল নাটকীয়।

অন্যরা বসে থাকেনি। একই বছর Mozilla আনে TraceMonkey (Firefox 3.5-এ চালু), যা বারবার চলা লুপের পথ “trace” করে compile করত। Apple আনে SquirrelFish Extreme (পরে নাম Nitro). এরপর কয়েক বছর ধরে প্রতিটি ব্রাউজার নতুন JIT, নতুন GC নিয়ে আসতে থাকে — Crankshaft, IonMonkey, এবং আরও অনেক।

এর প্রভাব বিশাল। দ্রুত engine-এর কারণে Node.js (২০০৯) সার্ভারে JavaScript নিয়ে যেতে পেরেছে। React, Angular-এর মতো বড় framework সম্ভব হয়েছে। VS Code-এর মতো desktop অ্যাপ (Electron) JavaScript-এ লেখা হচ্ছে। তবে একটি দাম আছে: JIT engine বিশাল জটিল সফটওয়্যার, আর তার বাগ থেকে নিরাপত্তা-ত্রুটি হয় — ব্রাউজারের অনেক গুরুতর নিরাপত্তা আপডেট আসে engine-এর JIT অংশ থেকেই।

মূল উপলব্ধি: প্রতিযোগিতা প্রযুক্তিকে দ্রুত এগিয়ে নেয়। একটি ভাষার সম্ভাবনা নির্ভর করে তার বাস্তবায়নের ওপরও — একই JavaScript ২০০৭ সালে যা পারত না, ২০১০ সালে তা পেরেছে, ভাষা প্রায় না বদলেই।

প্র ০৩JIT যত ভালোই হোক, কিছু কাজে JavaScript-এর গতি যথেষ্ট নয়। asm.js থেকে WebAssembly — এই পথটা কেমন ছিল?

JIT-এর একটি মৌলিক সীমা আছে: সে অনুমান করে। r number হবে — অনুমান; object-এর আকার একই থাকবে — অনুমান। অনুমান ভাঙলে deoptimization. গেম engine, ভিডিও এডিটর বা ছবি প্রসেসিংয়ের মতো কাজে এই অনিশ্চয়তা আর warm-up-এর সময় বড় সমস্যা।

২০১৩ সালে Mozilla একটি চতুর সমাধান দেখায় — asm.js. এটি JavaScript-এরই একটি কড়া উপসেট: যেমন x | 0 লিখে বোঝানো “এটা ৩২-বিট পূর্ণসংখ্যা”। এই কোড যেকোনো ব্রাউজারে চলত, কিন্তু যে engine asm.js চিনত, সে আগেভাগেই পুরোটা machine code-এ compile করে ফেলতে পারত — অনুমানের দরকার নেই, type লেখাই আছে। Emscripten নামের টুল দিয়ে C/C++ কোডকে asm.js-এ বদলানো যেত; এভাবে Unreal-এর মতো গেম engine-এর ডেমো ব্রাউজারে চালানো হয়েছিল।

কিন্তু asm.js ছিল একটি “হ্যাক”: ফাইল বিশাল (লেখা আকারে), parse করতে সময় লাগে। তাই চারটি বড় ব্রাউজার-নির্মাতা একসাথে বসে এর পরের ধাপ নকশা করে — WebAssembly (Wasm). এটি একটি compact binary format, যা দ্রুত decode হয় এবং আগেভাগে compile করা যায়। ২০১৭ সালে সব প্রধান ব্রাউজারে এটি চালু হয়, আর ২০১৯ সালে W3C এটিকে আনুষ্ঠানিক মানদণ্ড ঘোষণা করে।

লক্ষ করুন, Wasm JavaScript-কে সরিয়ে দেয়নি। দুটি পাশাপাশি চলে — একই engine (V8 একসাথে JavaScript আর Wasm চালায়)। পাতার যুক্তি, DOM আর UI থাকে JavaScript-এ; ভারী গণনার অংশ (ছবি প্রসেসিং, ভিডিও codec, গেম physics) Wasm-এ। Figma বা Google Earth-এর ওয়েব সংস্করণ এমন পদ্ধতির পরিচিত উদাহরণ।

মূল উপলব্ধি: নমনীয়তা আর পূর্বানুমেয় গতি একসাথে পাওয়া কঠিন। JavaScript নমনীয়তা বেছে নিয়েছে আর JIT দিয়ে গতি ফিরিয়ে আনে; যেখানে নিশ্চিত গতি লাগে, সেখানে Wasm. সঠিক কাজে সঠিক টুল।

প্র ০৪JavaScript কেন single-threaded থেকে গেল? Thread যোগ করলেই তো সব দ্রুত হতো!

JavaScript-এর জন্ম ব্রাউজারে, পাতার DOM বদলানোর জন্য। এখন ভাবুন, দুটি thread একসাথে একই বোতাম বদলাচ্ছে — একটি লেখা “কিনুন” করছে, আরেকটি বোতামটাই মুছে দিচ্ছে। কোনটা আগে হবে? এমন সমস্যাকে বলে race condition. Java বা C++-এ এর জন্য lock, mutex লাগে, আর তার ভুল থেকে আসে deadlock — দুটি thread একে অপরের জন্য চিরকাল অপেক্ষা করে। এসব বাগ খুঁজে পাওয়া কুখ্যাতভাবে কঠিন, কারণ প্রতিবার একই রকম ঘটে না।

JavaScript একটি সরল নিয়ম বেছে নিয়েছে — run-to-completion: একটি callback শুরু হলে শেষ না হওয়া পর্যন্ত অন্য কোনো JavaScript কোড মাঝখানে ঢুকতে পারে না। ফলে আপনি নিশ্চিত থাকেন, আপনার function চলার সময় অন্য কেউ চুপচাপ variable বদলে দেবে না। লক্ষ লক্ষ নতুন প্রোগ্রামারের জন্য এটি বিরাট স্বস্তি। দামটা হলো: লম্বা কাজ চালালে পুরো পাতা জমে যায় — অংশ ৬-এ যা দেখলেন।

তাহলে parallelism কীভাবে আসে? Web Worker দিয়ে (পর্ব ৪২)। একটি Worker সম্পূর্ণ আলাদা thread-এ চলে, তার নিজের call stack, নিজের heap, নিজের event loop. সে DOM ছুঁতে পারে না, আর main thread-এর সাথে কথা বলে বার্তা পাঠিয়ে (postMessage) — ডেটা কপি হয়ে যায়, ভাগ হয় না। ফলে race condition-এর সুযোগ খুব কম। Node.js-এ একই কাজ করে worker_threads.

যেখানে সত্যিই মেমরি ভাগ করা দরকার, সেখানে আছে SharedArrayBuffer আর Atomics (ES2017). তবে ২০১৮ সালে Spectre নামের CPU নিরাপত্তা-ত্রুটি ধরা পড়ার পর ব্রাউজারগুলো এটি কিছুদিন বন্ধ রাখে, এখন শুধু বিশেষ নিরাপত্তা-শর্ত (cross-origin isolation) মানলে চালু হয়।

মূল উপলব্ধি: “কম ক্ষমতা” কখনো কখনো ভালো নকশা। JavaScript ডিফল্টভাবে নিরাপদ, সরল মডেল দেয়, আর parallelism রাখে আলাদা ও স্পষ্ট জায়গায় — যাতে জটিলতা শুধু সেখানেই আসে, যেখানে সত্যিই দরকার।

প্র ০৫Garbage collector চলার সময় প্রোগ্রাম থেমে যায় — তাহলে গেম বা অ্যানিমেশন মসৃণ থাকে কীভাবে? Orinoco কী সমাধান দিল?

মসৃণ অ্যানিমেশনের জন্য ব্রাউজার সেকেন্ডে প্রায় ৬০ বার পাতা আঁকে — অর্থাৎ প্রতিটি ছবির জন্য মাত্র ১৬ মিলিসেকেন্ডের মতো সময়। পুরোনো “stop-the-world” GC পুরো heap চিহ্নিত ও পরিষ্কার করতে কখনো কখনো এর চেয়ে অনেক বেশি সময় নিত, আর সেই সময়ে JavaScript পুরোপুরি থেমে থাকত। ফল: অ্যানিমেশনে ঝাঁকুনি (jank).

V8-এর Orinoco প্রজেক্ট কয়েক বছর ধরে GC-কে নতুন করে সাজিয়েছে। মূল কৌশলগুলো:

  • Generational hypothesis: “বেশিরভাগ object অল্প বয়সেই মারা যায়।” তাই নতুন object রাখা হয় ছোট young generation-এ, যা খুব দ্রুত পরিষ্কার হয় — জীবিত অল্প কয়েকটিকে কপি করে সরিয়ে দিলেই বাকিটা একবারে খালি। টিকে যাওয়া object যায় old generation-এ।
  • Parallel: থামার সময়টুকুতে কাজ কয়েকটি helper thread-এ ভাগ করে করা — চারজন মিলে ঘর ঝাড়ু দিলে সময় কমে।
  • Incremental: বড় চিহ্নিতকরণের কাজ ছোট ছোট টুকরোয় ভাগ করে JavaScript-এর ফাঁকে ফাঁকে করা।
  • Concurrent: চিহ্নিতকরণের বড় অংশ পেছনের thread-এ, আপনার কোড চলার সাথে সাথেই। কঠিন অংশ হলো, কোড চলতে চলতে reference বদলায় — তাই engine “write barrier” দিয়ে সেই বদলগুলোর হিসাব রাখে।
  • Idle time: Chrome জানে কখন একটি ছবি আঁকা শেষ আর পরেরটির আগে ফাঁকা সময় আছে — GC-র কিছু কাজ সেই ফাঁকে সেরে নেয়।

ফলে আজ বেশিরভাগ পাতায় GC-র থামা চোখে পড়ে না। কিন্তু এটি জাদু নয়। প্রতি ফ্রেমে লাখ লাখ নতুন object তৈরি করলে (যেমন গেম লুপে প্রতিবার নতুন array) GC-কে বেশি খাটতে হয়। তাই উচ্চ-পারফরম্যান্স গেমে object পুনর্ব্যবহারের মতো কৌশল এখনো কাজে লাগে — তবে কেবল profiler সমস্যা দেখালে।

মূল উপলব্ধি: স্বয়ংক্রিয় মেমরি ব্যবস্থাপনা বিনামূল্যে আসে না — দাম দেওয়া হয় CPU, বাড়তি মেমরি আর মাঝে মাঝে থামা দিয়ে। আধুনিক GC সেই দামটা লুকিয়ে রাখতে পারদর্শী, কিন্তু বোঝার দায় এখনো প্রোগ্রামারের।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — হাতে-কলমে দেখা আর ভাবাই আসল কাজ।

  1. ক্রম অনুমান করুন: চালানোর আগে খাতায় লিখুন কোন ক্রমে লেখা আসবে, তারপর ব্রাউজারের console-এ চালিয়ে মেলান: setTimeout(() => console.log("x"), 0); console.log("y"); Promise.resolve().then(() => console.log("z"));

    ক্রম: y, z, x. y sync কোড, তাই সবার আগে। Script শেষে call stack খালি হলে event loop আগে microtask queue খালি করে — z. তারপর task queue থেকে setTimeout-এর callback — x. কোডে setTimeout সবার আগে লেখা থাকলেও কিছু যায় আসে না; কোন queue-তে যায়, সেটাই নির্ধারক।

  2. Stack trace পড়ুন: ব্রাউজারের console-এ লিখুন function a(){ b(); } function b(){ c(); } function c(){ console.trace("এখানে"); } a(); — যে তালিকা আসে, তাতে function-গুলোর ক্রম কী? কেন?

    তালিকায় ওপর থেকে নিচে আসে c, b, a, তারপর global/anonymous অংশ। এটি call stack-এর হুবহু ছবি: সবশেষে যে frame বসেছে (c) সেটি সবার ওপরে, আর সবার নিচে যে প্রথম ডাকা হয়েছিল। Error-এর stack trace-ও ঠিক এভাবে পড়তে হয় — ওপরের লাইনে ভুলের জায়গা, নিচের দিকে “কে ডেকেছিল” তার পথ।

  3. পাতা জমে যাওয়া নিজের চোখে: যেকোনো একটি সাধারণ পাতা খুলে console-এ চালান const end = Date.now() + 3000; while (Date.now() < end) {} — ৩ সেকেন্ড ধরে পাতায় scroll বা ক্লিক করার চেষ্টা করুন। কী দেখলেন, কেন?

    ৩ সেকেন্ড পাতা কোনো সাড়া দেয় না — ক্লিক কাজ করে না, লেখা select হয় না, অনেক পাতায় scroll-ও আটকে যায়। লুপটি main thread দখল করে রেখেছে, call stack কখনো খালি হচ্ছে না, তাই event loop কোনো ক্লিক-callback বা পাতা আঁকার কাজ চালাতে পারছে না। ৩ সেকেন্ড পর জমে থাকা ক্লিকগুলো একসাথে প্রক্রিয়া হতে পারে। (কিছু ব্রাউজার scroll আলাদা thread-এ সামলায়, তাই scroll কখনো কখনো চলতে পারে — কিন্তু JavaScript-নির্ভর সব কিছু থেমে থাকে।)

  4. ভাষা না runtime? নিচের প্রতিটি কোন স্তরের — ECMAScript, ব্রাউজার, না Node.js? (ক) Array.prototype.map (খ) document.querySelector (গ) process.argv (ঘ) Promise (ঙ) fetch

    (ক) ECMAScript — ভাষার built-in, সব জায়গায় চলে। (খ) ব্রাউজার — DOM, শুধু পাতায় আছে। (গ) Node.js — কমান্ড লাইনের argument; ব্রাউজারে process নেই। (ঘ) ECMAScript — ES2015 থেকে ভাষার অংশ। (ঙ) মূলত ব্রাউজারের Web API — তবে Node.js 18 থেকে Node-ও নিজের fetch দেয়। শেষেরটি দেখায়, runtime-গুলো একে অপরের ভালো API গ্রহণ করে, কিন্তু সেগুলো তবুও ভাষার অংশ নয়।

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

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

ক্রমV8-এ একটি function-এর যাত্রা সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✍️ Source code লেখা
  2. 🔤 Scanner — token-এ ভাঙা
  3. 🌳 Parser — AST তৈরি
  4. 📜 Ignition — bytecode বানিয়ে চালানো
  5. 🚀 গরম কোড — optimizing compiler-এ machine code

Chrome, Microsoft Edge আর Node.js — তিনটিতেই কোন JavaScript engine ব্যবহৃত হয়?

  • SpiderMonkey
  • V8
  • JavaScriptCore
  • Chakra

একটি JavaScript ফাইলের প্রথম লাইনে console.log("a") আর তৃতীয় লাইনে একটি SyntaxError আছে। ফাইলটি চালালে কী হবে?

  • “a” ছাপা হবে, তারপর error
  • তৃতীয় লাইন বাদ দিয়ে বাকি সব চলবে
  • কিছুই ছাপা হবে না, শুধু SyntaxError আসবে
  • কোনো error ছাড়াই প্রোগ্রাম থেমে যাবে

থামার শর্ত ছাড়া একটি function নিজেকে বারবার ডাকলে V8-এ কোন error আসে?

  • RangeError: Maximum call stack size exceeded
  • SyntaxError: too much recursion
  • ReferenceError: stack is not defined
  • TypeError: heap is full

setTimeout(f, 0), Promise.resolve().then(g) আর একটি সাধারণ console.log("h") — এই ক্রমে লেখা থাকলে কোনটি আগে চলবে?

  • f, g, h
  • g, h, f
  • h, f, g
  • h, g, f

Garbage collector কোন object মুছে ফেলার যোগ্য মনে করে?

  • যে object অনেকক্ষণ ব্যবহার হয়নি
  • Root থেকে কোনো reference-এর পথে যাকে আর পৌঁছানো যায় না
  • যে object-কে অন্য কোনো object দেখায় না, কেবল সেগুলো; চক্র কখনো মোছে না
  • যে object-এ প্রোগ্রামার delete চালিয়েছে

Node.js-এ document.title লিখলে ReferenceError: document is not defined আসে। এর আসল কারণ কী?

  • Node.js-এর engine JavaScript-এর পুরোনো version বোঝে
  • document ES2015-এর পরে ভাষা থেকে বাদ দেওয়া হয়েছে
  • document ভাষার অংশ নয় — এটি ব্রাউজার runtime দেয়, Node.js দেয় না
  • Node.js সবসময় strict mode-এ চলে বলে

JIT compiler একটি function-কে “r সবসময় number” ধরে optimize করেছে। হঠাৎ r-এ একটি string এলে engine কী করে?

  • Optimized কোড বাদ দিয়ে bytecode-এ ফিরে যায় (deoptimization), ফল সঠিকই আসে
  • Optimized কোড দিয়েই চালায়, ফল ভুল আসতে পারে
  • TypeError দিয়ে প্রোগ্রাম থামিয়ে দেয়
  • পুরো প্রোগ্রাম নতুন করে শুরু করে

শব্দকোষ

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

মনে রাখুন

  1. JavaScript চালায় একটি engine — V8 (Chrome, Edge, Node.js), SpiderMonkey (Firefox), JavaScriptCore (Safari).
  2. পাইপলাইন: token → AST → bytecode → (গরম হলে) machine code; অনুমান ভাঙলে deoptimization. একই আকারের object আর একই type রাখলে engine সহজে অনুমান করতে পারে।
  3. Function কল মানে call stack-এ একটি frame push, return মানে pop; সীমা ছাড়ালে RangeError: Maximum call stack size exceeded.
  4. Object থাকে heap-এ; root থেকে পৌঁছানো যায় না এমন object মোছে garbage collector (mark-and-sweep, generational). Reachable অথচ অপ্রয়োজনীয় object = memory leak.
  5. কোড চলে একটি thread-এ; লম্বা কাজ পাতা জমিয়ে দেয়। ক্রম: sync কোড → সব microtask → একটি task. ভাষা = ECMAScript, DOM/fs দেয় runtime.

পরের পর্বে: মডিউল ১ শেষ — engine কীভাবে কাজ করে, তা আপনি এখন জানেন। মডিউল ২ শুরু হবে ভাষার একদম মূল থেকে: JavaScript-এ মান আর টাইপ কী কী, আর "5" * 2 কেন 10 কিন্তু "5" + 2 কেন "52" — coercion-এর ফাঁদ।

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

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