এই পর্বে যা শিখবেন
- Synchronous বনাম asynchronous, আর blocking কোড কেন পুরো পাতা জমিয়ে দেয়
- Call stack, Web API, task queue, microtask queue আর rendering — কে কখন সুযোগ পায়
- যেকোনো “আউটপুট কোন ক্রমে আসবে” প্রশ্ন ধাপে ধাপে সমাধান করা
- Long task, INP, আর
setTimeout/scheduler.yield()দিয়ে ভারী কাজ ভাঙা - Node.js-এর event loop phase,
process.nextTickআরsetImmediate-এর ক্রম
১ · এক জন দোকানি, অনেক অর্ডার — synchronous বনাম asynchronous
পর্ব ৫-এ দেখেছেন, JavaScript engine কোড চালায় একটি call stack দিয়ে, আর একসময়ে একটিই কাজ করে। অথচ একটি সাধারণ ওয়েব পাতা একই সময়ে বোতামের ক্লিকের অপেক্ষা করে, একটি timer গুনছে, আর সার্ভার থেকে ডেটা আনছে। এক হাতে এত কাজ কীভাবে? এই পর্বের পুরো উত্তর একটি শব্দে — event loop. তবে শব্দটা মুখস্থ করার আগে ছবিটা পরিষ্কার করি।
পাড়ার চায়ের দোকানে একজনই দোকানি। কেউ দুধ-চা চাইলেন — দোকানি কেতলি চুলায় বসিয়ে দিলেন। পানি ফোটার জন্য তিনি কেতলির সামনে দাঁড়িয়ে থাকেন না; এই ফাঁকে আরেকজনকে বিস্কুট দেন, আরেকজনের টাকা গোনেন। পানি ফুটলে কেতলি শিস দেয়, আর হাতের কাজটা শেষ করে তিনি চা বানান। দোকানি = JavaScript-এর একমাত্র thread. চুলা আর কেতলি = ব্রাউজার বা Node.js, যারা অপেক্ষার কাজটা করে। শিস = “কাজ শেষ” খবর, যা লাইনে দাঁড়ায়। আর “হাতের কাজ শেষ করে তবেই পরেরটা” — এই নিয়মটিই event loop.
দুটি শব্দ আলাদা করে বুঝে নিন:
➡️ Synchronous
Synchronous কোড লাইনের পর লাইন চলে; একটি লাইন শেষ না হলে পরেরটি শুরু হয় না। const x = a + b; বা arr.sort() — ফল হাতে নিয়েই পরের লাইনে যায়।
🔔 Asynchronous
Asynchronous কাজ এখন শুরু হয়, ফল আসে পরে। setTimeout, fetch, ক্লিক শোনা — আপনি একটি callback বা Promise দিয়ে বলেন “শেষ হলে এটা চালাবেন”, আর কোড সাথে সাথে পরের লাইনে চলে যায়।
এখানে একটি সূক্ষ্ম কিন্তু জরুরি পার্থক্য আছে: concurrency আর parallelism. চায়ের দোকানি একসাথে পাঁচটি অর্ডার “চালু” রাখেন — এটা concurrency: অনেক কাজ একই সময়সীমায় এগোচ্ছে, কিন্তু যেকোনো মুহূর্তে হাত চলছে একটিতেই। Parallelism মানে সত্যিই একই মুহূর্তে দুটি কাজ — তার জন্য দ্বিতীয় দোকানি (আরেকটি thread বা CPU core) লাগে। JavaScript-এর main thread concurrency দেয়, parallelism নয়। Parallelism আসে Web Worker দিয়ে — পর্ব ৪২-এ।
JavaScript নিজে কখনো “অপেক্ষা” করে না। অপেক্ষা করে runtime — ব্রাউজারের Web API, বা Node.js-এর libuv. JavaScript-এর কাজ শুধু callback চালানো, একটির পর একটি, প্রতিটি শেষ পর্যন্ত। কোনটির পর কোনটি — সেই নিয়মের নাম event loop.
২ · Blocking — একটি লম্বা কাজ পুরো পাতা থামিয়ে দেয়
যেহেতু thread একটিই, কোনো একটি কাজ বেশিক্ষণ stack দখল করে রাখলে বাকি সবাইকে অপেক্ষা করতে হয়। একে বলে blocking. ব্রাউজারে এই thread-কে বলে main thread, আর সেখানে শুধু আপনার কোড নয়, ক্লিক সামলানো, style হিসাব, layout, পাতা আঁকা — সবই চলে। তাই main thread আটকানো মানে পাতাটাই আটকানো।
একটি অনলাইন দোকানের “অর্ডার দিন” বোতাম ভাবুন। ক্লিক করলে কোড কার্টের প্রতিটি পণ্যে ডিসকাউন্ট, ভ্যাট আর ডেলিভারি চার্জ হিসাব করে — ধরুন সব মিলিয়ে ৮০০ মিলিসেকেন্ড লাগে:
orderBtn.addEventListener("click", () => {
statusEl.textContent = "প্রসেস হচ্ছে…"; // ১. লেখা বদলাল (DOM-এ)
const total = calculateEverything(cart); // ২. ভারী হিসাব — ৮০০ ms
statusEl.textContent = `মোট: ${total} টাকা`; // ৩. আবার বদলাল
});এই কোড চালানোর জন্য নয় — ঘটনাটা বোঝার জন্য। অনেকে ভাবেন ক্লিকের সাথে সাথে “প্রসেস হচ্ছে…” দেখা যাবে, ৮০০ ms পরে মোট দাম। বাস্তবে “প্রসেস হচ্ছে…” লেখাটি কখনো পর্দায় আসে না। DOM-এ লেখা বদলানো আর পর্দায় আঁকা এক জিনিস নয়; আঁকার সুযোগ আসে কেবল callback শেষ হলে। ততক্ষণে লেখা আবার বদলে গেছে। আর এই ৮০০ ms ধরে ব্যবহারকারী যা-ই করুন — অন্য বোতামে ক্লিক, লেখা টাইপ — সব লাইনে দাঁড়িয়ে থাকে।
নিচের ডায়াগ্রামে দেখুন, ভারী কাজ চলার মাঝে একটি ক্লিক এলে ঠিক কী হয়:
| সময় | |
| Call stack | |
| DOM-এ লেখা | |
| Task queue | |
| পর্দায় যা দেখা যায় |
- ক্লিক এল। ব্যবহারকারী “অর্ডার দিন” চাপলেন। ব্রাউজার ক্লিকের task তৈরি করল, stack খালি ছিল, তাই
onOrderClickসাথে সাথে চলতে শুরু করল। - DOM বদলাল, পর্দা নয়। প্রথম লাইনে লেখা হলো “প্রসেস হচ্ছে…”. DOM-এ বদল হয়ে গেছে, কিন্তু পর্দা আঁকা হবে পরে — callback এখনো চলছে। তারপর শুরু হলো ভারী হিসাব।
- মাঝপথে আরেকটি ক্লিক। ২০০ ms-এ ব্যবহারকারী অধৈর্য হয়ে “বাতিল” চাপলেন। ব্রাউজারের অন্য অংশ ক্লিকটা ঠিকই টের পেল, কিন্তু JavaScript ব্যস্ত — তাই ক্লিকের task দাঁড়াল task queue-তে। কোনো কিছু হারায় না, শুধু অপেক্ষা করে।
- আরও জমছে। ৫০০ ms-এ একটি কী-চাপও লাইনে দাঁড়াল। পর্দা পুরোপুরি জমে আছে — বোতামে চাপ দেওয়ার কোনো দৃশ্যমান সাড়াও নেই। এটাই “অ্যাপ হ্যাং করেছে” অনুভূতি।
- হিসাব শেষ। ৮০০ ms-এ হিসাব শেষ হলো, শেষ লাইনে DOM-এ মোট দাম বসল। “প্রসেস হচ্ছে…” লেখাটি DOM-এ ছিল ঠিকই, কিন্তু একবারও আঁকা হয়নি।
- Stack খালি → পাতা আঁকা। Callback শেষ। Event loop microtask দেখল (নেই), তারপর ব্রাউজার পাতা আঁকল — সরাসরি মোট দাম। ক্লিক থেকে প্রথম দৃশ্যমান সাড়া পর্যন্ত লাগল প্রায় ৮০০ ms; এই দেরিকেই মাপে INP (অংশ ৬)।
- এবার জমে থাকা task. এতক্ষণে “বাতিল”-এর callback চলল — কিন্তু অর্ডার তো আগেই হিসাব হয়ে গেছে। ব্যবহারকারীর চোখে অ্যাপটি “কথা শোনে না”। ভারী কাজ টুকরো করলে এই সমস্যা কমে — অংশ ৬-এ দেখবেন।
আপনার ব্রাউজারের console-এ নিচের কোডটি চালিয়ে দেখতে পারেন। সময় মাপার দরকার নেই — শুধু লেখার ক্রম দেখুন:
setTimeout(() => console.log("৩. timer — ০ ms চেয়েছিলাম"), 0);
let sum = 0;
for (let i = 0; i < 200_000_000; i++) sum += i; // কয়েকশো ms ব্যস্ত
console.log("১. লুপ শেষ");
console.log("২. script শেষ");
// প্রত্যাশিত আউটপুট:
// ১. লুপ শেষ
// ২. script শেষ
// ৩. timer — ০ ms চেয়েছিলামTimer-টি কোডে সবার আগে লেখা, আর চাওয়া হয়েছে ০ ms. তবু সেটি এসেছে সবার শেষে। Timer-এর সময় লুপ চলার সময়েই পেরিয়ে গেছে, callback task queue-তে দাঁড়িয়েও গেছে — কিন্তু stack খালি না হওয়া পর্যন্ত কেউ তাকে ঢুকতে দেয়নি। লুপ যত লম্বাই হোক, ক্রম একই থাকবে; শুধু অপেক্ষা বাড়বে। এই কারণেই বলা হয়, timer-এর সময় “অন্তত এতক্ষণ”, “ঠিক এতক্ষণ” নয়।
৩ · Event loop-এর অংশগুলো — stack, host, দুই রকম queue আর rendering
এবার পুরো যন্ত্রটির অংশগুলো নাম ধরে চিনে নিই। ব্রাউজারে event loop-এর নিয়ম লেখা আছে HTML Standard-এ (WHATWG); ECMAScript ভাষা শুধু বলে Promise-এর কাজ একটি “job” queue-তে যাবে, কখন চলবে তা ঠিক করে host.
| অংশ | কী করে | উদাহরণ |
|---|---|---|
| Call stack | এখন যে JavaScript চলছে তার frame-গুলো; একটিমাত্র | আপনার script, যেকোনো callback |
| Host / Web API (Node.js-এ libuv) | অপেক্ষার কাজ করে — JavaScript-এর thread-এর বাইরে | timer গোনা, নেটওয়ার্ক, ক্লিক শোনা, ফাইল পড়া |
| Task queue (macrotask) | শেষ হওয়া কাজের callback-এর লাইন; event loop প্রতি ঘুরে একটি নেয় | setTimeout, setInterval, ক্লিক, postMessage, script লোড |
| Microtask queue | অগ্রাধিকারের লাইন; প্রতিটি task-এর পরে পুরো খালি করা হয় | Promise-এর then/catch/finally, await-এর পরের অংশ, queueMicrotask, MutationObserver |
| Rendering | দরকার হলে style, layout, paint — সাধারণত সেকেন্ডে ৬০ বার পর্যন্ত (স্ক্রিনের refresh rate অনুযায়ী) | আঁকার ঠিক আগে requestAnimationFrame callback |
লক্ষ করুন, task queue আসলে একাধিক থাকতে পারে — timer-এর একটি, ব্যবহারকারীর ইনপুটের একটি, নেটওয়ার্কের একটি। Spec ব্রাউজারকে স্বাধীনতা দেয় কোন queue থেকে পরের task নেবে; তাই অনেক ব্রাউজার ক্লিক বা কী-চাপের মতো ইনপুটকে আগে সুযোগ দেয়। কিন্তু একই queue-এর ভেতরে ক্রম সবসময় আগে-এলে-আগে।
পুরো নিয়মটা একটি লুপ হিসেবে লিখলে এমন দাঁড়ায় (সরল ছদ্মকোড, আসল কোড নয়):
while (true) {
const task = pickOldestTask(); // একটি task (না থাকলে অপেক্ষা)
if (task) run(task); // শেষ পর্যন্ত চালাও — মাঝে কেউ ঢুকবে না
while (microtaskQueue.length > 0) { // সব microtask, নতুন যোগ হলেও
run(microtaskQueue.shift());
}
if (timeToRender()) { // সাধারণত ~১৬ ms পরপর
runAnimationFrameCallbacks(); // requestAnimationFrame
styleLayoutPaint();
}
}তিনটি জিনিস মনে রাখুন। প্রথমত, প্রতি ঘুরে task মাত্র একটি, কিন্তু microtask সবগুলো — এমনকি চলতে চলতে নতুন microtask যোগ হলে সেটিও। দ্বিতীয়ত, প্রথম “task” হলো আপনার পুরো script; তাই script শেষ হলেই প্রথমবার microtask চলে। তৃতীয়ত, rendering-এর সুযোগ আসে task আর microtask দুটোই শেষ হওয়ার পর — তার আগে কোনোভাবেই নয়।
প্রতিটি callback শেষ পর্যন্ত চলে, মাঝখানে অন্য কোনো JavaScript ঢুকতে পারে না — একে বলে run-to-completion. এটি একটি বড় সুবিধা: আপনার function চলার সময় অন্য কোনো callback এসে variable বদলে দেবে না, তাই Java বা C++-এর মতো lock লাগে না। আবার এটিই blocking-এর কারণ।
requestAnimationFrame — আঁকার ঠিক আগের মুহূর্ত
অ্যানিমেশনের জন্য setTimeout(draw, 16) লেখা পুরোনো অভ্যাস। Timer পর্দার আঁকার সাথে তাল মেলায় না — কখনো এক ফ্রেমে দুবার চলে, কখনো একবারও না, ফলে ঝাঁকুনি। requestAnimationFrame (rAF) ব্রাউজারকে বলে, “পরের বার আঁকার ঠিক আগে এটা চালাবেন”। তাই প্রতি ফ্রেমে ঠিক একবার চলে, আর tab লুকানো থাকলে সাধারণত থেমে থাকে — ব্যাটারিও বাঁচে। বিস্তারিত অ্যানিমেশন এই কোর্সের বিষয় নয়; শুধু মনে রাখুন rAF event loop-এর rendering ধাপের অংশ, task বা microtask নয়।
৪ · ক্রমের ধাঁধা — ধাপে ধাপে সমাধান
পর্ব ৫-এ একটি সহজ ধাঁধা দেখেছিলেন: sync → Promise → setTimeout. এবার একটু কঠিন রূপ — একটি task-এর ভেতর থেকে microtask তৈরি হলে কী হয়? দ্বিতীয় timer আগে চলবে, না নতুন microtask? চালানোর আগে নিজে খাতায় উত্তর লিখুন, তারপর “পরে” চাপুন।
1console.log("A");2setTimeout(() => {3 console.log("D");4 queueMicrotask(() => console.log("E"));5}, 0);6setTimeout(() => console.log("F"), 0);7Promise.resolve().then(() => console.log("C"));8console.log("B");
| Call stack | |
| Web API | |
| Microtask | |
| Task queue |
- পুরো script নিজেই প্রথম task. লাইন ১ সরাসরি চলল —
A. - প্রথম timer ব্রাউজারের হাতে।
setTimeoutcallback-টি timer-কে দিয়ে সাথে সাথে ফিরে এল। ০ ms পেরোতেই callback (T1) দাঁড়াল task queue-তে। কিন্তু stack এখনো script-এর দখলে। - দ্বিতীয় timer. লাইন ৬-এর callback (T2) একই লাইনে, T1-এর পেছনে। একই queue — আগে এলে আগে।
- Promise-এর then → microtask. Promise আগেই fulfilled, তাই
then-এর callback (C) সাথে সাথে microtask queue-তে গেল। চলবে না এখনই — script তো শেষ হয়নি। - Script-এর শেষ লাইন।
Bছাপা হলো। এখন প্রথম task (পুরো script) শেষ, call stack খালি। - সব microtask. Event loop আগে microtask queue খালি করে —
C. Queue এখন খালি। এখানে ব্রাউজার চাইলে পাতা আঁকতে পারে। - একটি task — T1. Task queue থেকে সবচেয়ে পুরোনোটি: T1 চলল, ছাপল
D, আরqueueMicrotaskদিয়ে নতুন microtask E যোগ করল। T2 এখনো লাইনে। - T1 শেষ → আবার সব microtask. প্রশ্নের আসল উত্তর এখানে: T2-এর আগে চলে E, কারণ প্রতিটি task-এর পরে microtask queue খালি করা হয়। ছাপা হলো
E. - তারপর T2. এবার পরের task —
F. সব queue খালি; event loop নতুন কাজের অপেক্ষায় ঘুমিয়ে থাকে।
এবার একটি গল্পে বাঁধা উদাহরণ নিজে চালিয়ে দেখুন। একটি অনলাইন দোকানে অর্ডার এলে কয়েকটি কাজ হয় — কিছু সাথে সাথে, কিছু microtask হিসেবে, কিছু timer দিয়ে। এখানে async function আর await-ও আছে:
console.log("অর্ডার এল");
setTimeout(() => {
console.log("কুরিয়ারকে খবর (task ১)");
Promise.resolve().then(() => console.log("গ্রাহককে SMS (task ১-এর microtask)"));
}, 0);
setTimeout(() => console.log("রিভিউ চাওয়া (task ২)"), 0);
queueMicrotask(() => console.log("স্টক কমানো (queueMicrotask)"));
async function pay() {
console.log("পেমেন্ট শুরু (async function-এর sync অংশ)");
await null;
console.log("পেমেন্ট নিশ্চিত (await-এর পরের অংশ)");
}
pay();
Promise.resolve()
.then(() => console.log("ইনভয়েস তৈরি (then ১)"))
.then(() => console.log("ইমেইল পাঠানো (then ২)"));
console.log("script শেষ");আউটপুটের ক্রম: অর্ডার এল → পেমেন্ট শুরু → script শেষ → স্টক কমানো → পেমেন্ট নিশ্চিত → ইনভয়েস তৈরি → ইমেইল পাঠানো → কুরিয়ারকে খবর → গ্রাহককে SMS → রিভিউ চাওয়া. তিনটি বিষয় খেয়াল করুন। (১) async function ডাকলে তার প্রথম await পর্যন্ত অংশ synchronous চলে — তাই “পেমেন্ট শুরু” এসেছে “script শেষ”-এর আগে। (২) await-এর পরের অংশ একটি microtask; সেটি লাইনে ঢুকেছে স্টক কমানোর পরে, ইনভয়েসের আগে — যে ক্রমে লাইনে ঢুকেছে, সেই ক্রমেই চলেছে। “ইমেইল পাঠানো” ঢুকেছে আরও পরে, কারণ দ্বিতীয় then লাইনে যায় প্রথমটি শেষ হলে। (৩) SMS-টি “রিভিউ চাওয়া”-র আগে, কারণ task ১ শেষ হতেই তার microtask চলে। async/await-এর বিস্তারিত পর্ব ৩৪-এ।
খাতায় তিনটি কলাম আঁকুন: আউটপুট, Microtask, Task. কোডটি ওপর থেকে পড়ুন — sync লেখা সরাসরি আউটপুটে; then/await-এর পরের অংশ/queueMicrotask Microtask কলামে; timer আর event Task কলামে। Script শেষে Microtask কলাম ওপর থেকে খালি করুন (নতুন কিছু এলে নিচে যোগ করুন), তারপর Task কলাম থেকে একটি তুলুন, আবার Microtask খালি করুন। এই নিয়মে ভুল হয় না।
৫ · Microtask-এর ফাঁদ, আর setTimeout 0-এর আসল মানে
“প্রতিটি task-এর পরে সব microtask” — নিয়মটির একটি অন্ধকার দিক আছে। একটি microtask যদি আরেকটি microtask যোগ করে, আর সেটি আরেকটি… তাহলে queue কখনো খালি হয় না। Event loop সেখানেই আটকে থাকে: কোনো task চলে না, পাতাও আঁকা হয় না। একে বলে microtask starvation. নিচের ডায়াগ্রামে দুটি “লুপ” তুলনা করুন — একটি queueMicrotask দিয়ে, আরেকটি setTimeout দিয়ে।
| পদ্ধতি | |
| এখন চলছে | |
| Microtask | |
| Task queue | |
| পাতা আঁকা |
- Microtask লুপ শুরু।
function tick() { count++; queueMicrotask(tick); }— প্রথম tick চলল, আর নিজের পরের কপি microtask queue-তে রেখে গেল। এর মধ্যে ব্যবহারকারী একটি বোতামে ক্লিক করেছেন; ক্লিকটি task queue-তে। - Queue কখনো খালি হয় না। Event loop নিয়ম মেনে microtask খালি করার চেষ্টা করছে, কিন্তু প্রতিটি tick আরেকটি tick যোগ করছে। “সব microtask শেষ” মুহূর্তটি কখনো আসে না।
- পাতা জমে গেছে। লাখের পর লাখ tick. ক্লিক এখনো অপেক্ষায়, পাতা একবারও আঁকা হয়নি। এটি একটি
while(true)লুপের মতোই খারাপ — শুধু দেখতে নিরীহ। কিছুক্ষণ পর ব্রাউজার “Page unresponsive” জাতীয় সতর্কতা দেখাতে পারে। - এবার task দিয়ে লুপ।
setTimeout(tick, 0)দিয়ে একই কাজ। প্রথম tick একটি task হিসেবে চলল, পরের tick-কে task queue-তে পাঠাল — ক্লিকের পেছনে। - Task শেষ → ফাঁকা মুহূর্ত। Tick শেষ, microtask নেই। এই ফাঁকে event loop-এর rendering ধাপ আসে — সময় হলে পাতা আঁকা হয়।
- সবাই পালা পায়। পরের task হিসেবে চলল ক্লিকের callback, তারপর tick #২. পাতা সাড়া দিচ্ছে। দাম একটাই: প্রতি ঘুরে একটু দেরি (নিচে ৪ ms-এর নিয়ম দেখুন)। ভারী কাজ ভাঙার পুরো কৌশল এই পার্থক্যের ওপর দাঁড়িয়ে।
একটি প্রচলিত ভুল: অনেকে ভারী লুপের মাঝে await Promise.resolve() বা await null লিখে ভাবেন “main thread ছেড়ে দিলাম”। দেননি। await-এর পরের অংশ একটি microtask, আর microtask চলে পাতা আঁকা আর ক্লিকের আগেই। পাতা আগের মতোই জমে থাকবে। সত্যিই ছাড়তে হলে একটি task লাগে — setTimeout, MessageChannel, বা scheduler.yield().
setTimeout(fn, 0) — “এখনই” নয়, “পরের কোনো task-এ”
setTimeout-এর দেরি একটি ন্যূনতম সময়, নিশ্চিত সময় নয়। বাস্তবে আরও কয়েকটি নিয়ম আছে:
- নেস্টিংয়ের সীমা: HTML Standard অনুযায়ী timer-এর ভেতর থেকে timer, তার ভেতর থেকে আবার timer — এভাবে নেস্টিং পাঁচ স্তর ছাড়ালে ৪ ms-এর কম দেরি চাইলেও ব্রাউজার অন্তত ৪ ms ধরে নেয়। একে বলে timer clamping. উপরের task-লুপ তাই কয়েকবার পরেই প্রতি ঘুরে অন্তত ৪ ms নেয়।
- লুকানো tab: পেছনের tab-এ ব্রাউজার timer অনেক ধীর করে দেয় (প্রায়ই সেকেন্ডে একবার বা তারও কম) — ব্যাটারি বাঁচাতে।
- Node.js:
setTimeout(fn, 0)আসলে ১ ms ধরা হয়; দেরি ১-এর কম বা ২৩১−১ ms-এর বেশি দিলে Node সেটিকে ১ করে দেয়। - সবার ওপরে: stack ব্যস্ত থাকলে কোনো timer-ই চলতে পারে না — অংশ ২-এর লুপের উদাহরণ মনে করুন।
তাহলে setTimeout(fn, 0) কেন লেখা হয়? মানে হলো: “চলমান কাজটা শেষ হোক, ব্রাউজার একবার শ্বাস নিক (আঁকা, ইনপুট), তারপর আমাকে চালাবেন”। কাজ পিছিয়ে দেওয়ার এটি পুরোনো ও কার্যকর উপায় — শুধু “০” সংখ্যাটা বিভ্রান্তিকর। Timer আর callback-এর বিস্তারিত পর্ব ৩২-এ।
queueMicrotask(fn) এসেছে সরাসরি microtask যোগ করার জন্য — আগে লোকে Promise.resolve().then(fn) লিখে কাজ চালাত। দুটোর ক্রম একই; পার্থক্য শুধু error-এ: queueMicrotask-এর callback-এ error হলে সেটি সাধারণ uncaught error হিসেবে আসে, আর Promise-এর ভেতরে হলে rejected Promise হয়ে যায়। কখন ব্যবহার করবেন? খুব কম — যখন চলমান কোড শেষ হওয়ার ঠিক পরে, কিন্তু আঁকার আগে কিছু করা দরকার (যেমন কয়েকটি বদল একসাথে জমিয়ে একবারে প্রকাশ করা)।
৬ · Long task, INP আর ভারী কাজ টুকরো করা
Main thread-কে ৫০ মিলিসেকেন্ডের বেশি আটকে রাখা যেকোনো task-কে বলে long task. কেন ৫০? সাধারণ নির্দেশিকা হলো ক্লিকের সাড়া ১০০ ms-এর মধ্যে দেখা গেলে মানুষ সেটিকে “তাৎক্ষণিক” মনে করে; task ৫০ ms-এর মধ্যে রাখলে বাকি অর্ধেক সময় ইনপুট সামলানো আর আঁকার জন্য থাকে।
Google-এর Core Web Vitals-এ সাড়া দেওয়ার মাপকাঠি এখন INP (Interaction to Next Paint) — ২০২৪ সালের মার্চে এটি FID-এর জায়গা নিয়েছে। INP মাপে: পাতায় থাকার পুরো সময়ের ক্লিক, ট্যাপ আর কী-চাপের মধ্যে প্রায় সবচেয়ে ধীরটিতে, ইনপুট থেকে পরের বার পর্দা আঁকা পর্যন্ত কত সময় লাগল। ২০০ ms বা কম হলে “ভালো”, ৫০০ ms-এর বেশি হলে “খারাপ”। অংশ ২-এর “অর্ডার দিন” বোতামের INP প্রায় ৮০০ ms — স্পষ্টতই খারাপ। DevTools দিয়ে INP আর long task খুঁজে বের করা শিখবেন পর্ব ৫৩-এ।
INP-এর সময় তিন ভাগে ভাগ হয়: input delay (অন্য task চলছিল বলে আপনার handler শুরু হতে দেরি), processing (আপনার handler নিজে), আর presentation delay (style, layout, paint). তিনটির প্রতিটিই long task দিয়ে বাড়ে।
সমাধান: কাজ টুকরো করুন, মাঝে মাঝে main thread ছেড়ে দিন
পুরো হিসাব একবারে না করে ছোট ছোট টুকরোয় করুন, আর প্রতি টুকরোর পর একটি নতুন task-এ চলে যান — যাতে ফাঁকে ব্রাউজার ক্লিক সামলাতে আর পাতা আঁকতে পারে। একে বলে main thread-কে ছেড়ে দেওয়া (yielding), আর কাজ ভাঙাকে chunking:
function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield(); // নতুন ব্রাউজারে
return new Promise((resolve) => setTimeout(resolve, 0)); // বাকি সবখানে
}
orderBtn.addEventListener("click", async () => {
statusEl.textContent = "প্রসেস হচ্ছে…";
await yieldToMain(); // আগে লেখাটা পর্দায় আসুক
let total = 0, last = performance.now();
for (const item of cart) {
total += priceWithDiscount(item);
if (performance.now() - last > 40) { // ~৪০ ms পরপর বিরতি
await yieldToMain();
last = performance.now();
}
}
statusEl.textContent = `মোট: ${total} টাকা`;
});এটিও ব্রাউজারের আসল পাতার জন্য লেখা উদাহরণ, এখানে চালানোর জন্য নয়। এখন ক্লিকের পর প্রথম await yieldToMain()-এর ফাঁকে ব্রাউজার “প্রসেস হচ্ছে…” এঁকে ফেলে — ব্যবহারকারী সাথে সাথে সাড়া পান, INP ভালো হয়। তারপর প্রতি ~৪০ ms কাজের পর একটি ফাঁক, যেখানে অন্য ক্লিক বা scroll সামলানো যায়। মোট সময় একটু বাড়ে, কিন্তু পাতা আর জমে না। সংখ্যা গুনে (প্রতি ৫০০টি পণ্যে) না থেমে সময় দেখে থামা ভালো, কারণ ধীর ফোনে একই সংখ্যক পণ্যে বেশি সময় লাগে। খেয়াল রাখুন: কাজ চলার মাঝে কার্ট বদলে গেলে কী হবে, সেটাও এখন ভাবতে হবে — টুকরো করার এটাই দাম।
| উপায় | কীভাবে কাজ করে | খেয়াল রাখুন |
|---|---|---|
setTimeout(r, 0) | নতুন task — সব জায়গায় চলে | নেস্টিংয়ে ৪ ms clamp; বাকি কাজ task queue-এর পেছনে চলে যায়, অন্য script আগে ঢুকতে পারে |
scheduler.yield() | Main thread ছেড়ে দেয়, কিন্তু বাকি কাজকে সাধারণ task-এর চেয়ে আগে চালানোর অগ্রাধিকার দেয় | Chrome ও Edge 129 (২০২৪) থেকে; সব ব্রাউজারে নেই — তাই উপরের মতো feature detection রাখুন |
requestIdleCallback | ব্রাউজার যখন ফাঁকা, তখন চালায় | জরুরি নয় এমন কাজের জন্য (analytics পাঠানো, আগে থেকে ডেটা সাজানো); সব ব্রাউজারে সমর্থন সমান নয় |
| Web Worker | সম্পূর্ণ আলাদা thread-এ চালায় — সত্যিকারের parallelism | DOM ছুঁতে পারে না; ডেটা বার্তা দিয়ে আদান-প্রদান। পর্ব ৪২ |
কাজটা ছোট ছোট স্বাধীন ধাপে ভাঙা যায় আর DOM-এর সাথে জড়িত — chunking. কাজটা বিশুদ্ধ হিসাব (ছবি প্রসেসিং, বড় CSV বিশ্লেষণ, encryption) আর কয়েকশো ms-এর বেশি — Web Worker. আর কোনোটা করার আগে মাপুন: অনেক সময় আসল সমস্যা হিসাব নয়, অপ্রয়োজনে হাজারটা DOM node বারবার বানানো।
৭ · Node.js-এর event loop — phase, nextTick আর setImmediate
Node.js-এও একই মূল চিন্তা: আপনার JavaScript একটি thread-এ, অপেক্ষার কাজ বাইরে। এখানে বাইরের কাজ করে libuv নামের একটি C library — নেটওয়ার্কের জন্য অপারেটিং সিস্টেমের ব্যবস্থা (Linux-এ epoll, macOS-এ kqueue, Windows-এ IOCP), আর ফাইল পড়া, DNS খোঁজা বা কিছু crypto-র মতো কাজের জন্য একটি ছোট thread pool (ডিফল্ট ৪টি thread). তবে Node-এর loop ব্রাউজারের চেয়ে বেশি কাঠামোবদ্ধ — প্রতিটি ঘুর কয়েকটি নির্দিষ্ট phase-এ ভাগ করা:
| Phase | কী চলে |
|---|---|
| timers | সময় পেরিয়ে যাওয়া setTimeout / setInterval-এর callback |
| pending callbacks | আগের ঘুরে পিছিয়ে রাখা কিছু সিস্টেম-স্তরের I/O callback (যেমন কিছু TCP error) |
| idle, prepare | শুধু Node-এর ভেতরের ব্যবহারের জন্য |
| poll | নতুন I/O ঘটনা আনা আর তাদের callback চালানো (ফাইল পড়া শেষ, নতুন request এল); কাজ না থাকলে এখানে অপেক্ষা করে |
| check | setImmediate-এর callback — poll-এর ঠিক পরে |
| close callbacks | socket.on("close")-এর মতো বন্ধ হওয়ার callback |
আর প্রতিটি callback শেষ হওয়ার পরে, পরের callback-এর আগে, Node দুটি লাইন খালি করে — প্রথমে process.nextTick-এর queue, তারপর Promise-এর microtask queue. (Node 11 থেকে এটি প্রতিটি timer বা immediate callback-এর পরেই হয়, ব্রাউজারের মতো; তার আগে পুরো phase শেষে হতো।) নামের সাথে কাজের মিল নেই, খেয়াল করুন: nextTick আসলে “পরের tick”-এ নয়, চলমান কাজের ঠিক পরেই চলে; আর setImmediate “এখনই” নয়, check phase-এ চলে। Node-এর ডকুমেন্টেশনও স্বীকার করে নামগুলো উল্টো হওয়া উচিত ছিল।
const fs = require("fs");
console.log("A: sync কোড শুরু");
setTimeout(() => console.log("E: timers phase — setTimeout"), 0);
Promise.resolve().then(() => console.log("D: promise microtask"));
process.nextTick(() => console.log("C: process.nextTick"));
console.log("B: sync কোড শেষ");
fs.readFile(__filename, () => {
console.log("F: poll phase — ফাইল পড়া শেষ");
setTimeout(() => console.log("J: পরের ঘুরে timers"), 0);
setImmediate(() => console.log("I: check phase — setImmediate"));
Promise.resolve().then(() => console.log("H: promise"));
process.nextTick(() => console.log("G: nextTick"));
});আউটপুট বর্ণমালার ক্রমে: A থেকে J. প্রথমে দুটি sync লাইন (A, B). Script শেষ হলে nextTick queue (C), তারপর promise (D) — Promise কোডে আগে লেখা থাকলেও। তারপর loop-এর প্রথম timers phase-এ E. ফাইল পড়া কয়েক ঘুর নেয়; শেষ হলে poll phase-এ F. সেই callback-এর ভেতরে চারটি নতুন কাজ: আগে nextTick (G), তারপর promise (H). এরপর loop poll থেকে সোজা check phase-এ যায় — তাই setImmediate (I) চলে setTimeout-এর আগে, যদিও দুটিই “০”। Timer (J) চলে পরের ঘুরের timers phase-এ। কোনো কাজ বাকি না থাকায় process নিজে থেকে বন্ধ হয়।
একটি ক্রম কখনো নিশ্চিত নয়: মূল script-এ (কোনো I/O callback-এর বাইরে) setTimeout(fn, 0) আর setImmediate(fn) পাশাপাশি লিখলে কোনটি আগে চলবে তা নিশ্চিত নয় — loop শুরু হতে ১ ms পেরিয়েছে কি না, তার ওপর নির্ভর করে। তাই উপরের কোডে সেই জোড়া আমরা মূল অংশে রাখিনি। I/O callback-এর ভেতরে কিন্তু setImmediate সবসময় আগে। আরেকটি সূক্ষ্মতা: ফাইলটি ES module (.mjs বা "type": "module") হলে মূল অংশে promise (D) আগে আসে, nextTick (C) পরে — কারণ ESM-এর মূল কোড নিজেই একটি promise-এর কাজ হিসেবে চলে। Module নিয়ে পর্ব ৪৫-এ।
| এখন | |
| nextTick queue | |
| Promise microtask | |
| অপেক্ষায় |
- মূল script. A আর B ছাপা হলো। পথে একটি timer (E) libuv-কে, একটি Promise callback (D) microtask-এ, একটি nextTick (C), আর ফাইল পড়ার কাজ thread pool-এ গেল।
- nextTick সবার আগে। Script শেষ হতেই Node প্রথমে nextTick queue খালি করে — C. (CommonJS ফাইলে এই নিয়ম।)
- তারপর promise. nextTick খালি হলে promise microtask — D. এখন loop শুরু হবে।
- timers phase. ১ ms পেরিয়ে গেছে, তাই E চলল। ফাইল এখনো পড়া হচ্ছে — open, read, close-এর প্রতিটি ধাপ thread pool-এ আলাদাভাবে হয়।
- poll phase — I/O callback. কয়েক ঘুর পর ফাইল পড়া শেষ; poll phase-এ callback চলল — F. ভেতরে চারটি নতুন কাজ যোগ হলো।
- Callback শেষ → দুই লাইন খালি। আগে nextTick (G), তারপর promise (H) — প্রতিটি callback-এর পরে এই নিয়ম।
- check phase. poll-এর ঠিক পরেই check — setImmediate-এর I. Timer J-কে অপেক্ষা করতে হবে পরের ঘুর পর্যন্ত।
- পরের ঘুরের timers. J চলল। আর কোনো timer, I/O বা immediate বাকি নেই — loop থেমে যায়, process বন্ধ হয়। একটি সার্ভার কেন বন্ধ হয় না? কারণ খোলা socket সবসময় “অপেক্ষায়” থাকে।
এক thread-এ হাজার request — আর সবচেয়ে বড় ভুল
একটি Node.js সার্ভারে প্রতিটি request আসে একটি task হিসেবে (poll phase-এ)। Handler ডেটাবেসকে প্রশ্ন পাঠিয়ে await করলে thread মুক্ত হয়ে যায়, আর পরের request সামলাতে পারে। তাই একটি thread-ই একসাথে হাজার হাজার সংযোগ ধরে রাখতে পারে — যতক্ষণ প্রত্যেকে বেশিরভাগ সময় অপেক্ষাই করে। বিকাশ-এর মতো কোনো পেমেন্ট সেবার কাউন্টারে একজন কর্মী যদি প্রতিটি গ্রাহকের ফর্ম জমা দিয়ে উত্তর আসা পর্যন্ত বসে না থেকে পরের জনকে ডাকেন — সেই রকম।
const http = require("node:http");
const crypto = require("node:crypto");
http.createServer((req, res) => {
// ❌ sync hash: প্রতিটি request-এ main thread কয়েকশো ms আটকে যায়
const hash = crypto.pbkdf2Sync("pass", "salt", 600_000, 64, "sha512");
res.end(hash.toString("hex").slice(0, 16));
}).listen(3000);
// ✅ async রূপ: ভারী কাজ libuv-এর thread pool-এ, main thread মুক্ত
// crypto.pbkdf2("pass", "salt", 600_000, 64, "sha512", (err, hash) => { ... });এটি চালানোর জন্য নয়, শুধু তুলনার জন্য। প্রথম রূপে একজন ব্যবহারকারী লগইন করার সময় বাকি সব ব্যবহারকারীর request — এমনকি একটি ছোট ছবি চাওয়াও — অপেক্ষা করে, কারণ thread একটিই। দ্বিতীয় রূপে hash হিসাব চলে thread pool-এ, আর main thread অন্য request সামলায়। Node.js-এ নিয়ম একটাই: request handler-এ কখনো CPU-ভারী sync কাজ নয়। নামের শেষে Sync থাকা function (যেমন readFileSync) সার্ভার শুরু হওয়ার সময় চলতে পারে, request-এর পথে নয়। Node.js নিয়ে বিস্তারিত পর্ব ৪৯ আর ৫০-এ।
Python-এর asyncio প্রায় একই মডেল — একটি thread-এ একটি event loop, await-এ coroutine থামে, আর CPU-ভারী কাজ loop আটকে দেয় (Python-এর asyncio). Java ঐতিহ্যগতভাবে উল্টো পথে চলে — প্রতি request-এ একটি thread, এখন হালকা virtual thread দিয়ে লাখ লাখ (Java-র virtual thread); সেখানে race condition আর lock নিয়ে ভাবতে হয়, JavaScript-এ সাধারণত নয়। C/C++-এ select/epoll দিয়ে নিজেই event loop লিখতে হয় — libuv ঠিক সেটাই আপনার হয়ে করে (C++-এ নেটওয়ার্কিং)।
- sync লগ,
setTimeout 0,Promise.then— কোন ক্রমে আসবে? — আগে সব sync, তারপর সব microtask (Promise), তারপর task (setTimeout). এটিই JavaScript ইন্টারভিউয়ের সবচেয়ে প্রচলিত প্রশ্নগুলোর একটি। - Microtask আর macrotask-এর পার্থক্য? — প্রতি ঘুরে task একটি, কিন্তু তার পরে microtask queue পুরো খালি করা হয়; তাই microtask সবসময় পরের task-এর আগে।
- JavaScript single-threaded হলে asynchronous কীভাবে? — অপেক্ষার কাজ করে runtime (Web API / libuv); শেষ হলে callback queue-তে, আর stack খালি হলে event loop তা চালায়।
process.nextTickবনামsetImmediate? — nextTick চলমান কাজের ঠিক পরে, promise-এরও আগে; setImmediate চলে loop-এর check phase-এ, poll-এর পরে।- একটি ভারী লুপ পাতা জমিয়ে দিচ্ছে — কী করবেন? — কাজ টুকরো করে মাঝে task দিয়ে main thread ছেড়ে দেওয়া (setTimeout / scheduler.yield), বা Web Worker-এ পাঠানো।
৮ · async/await-ও microtask, আর event loop চোখে দেখার উপায়
মডিউল ৫-এর বাকি পর্বগুলোর সবকিছু এই পর্বের নিয়মের ওপর দাঁড়িয়ে। পর্ব ৩২-এর callback আর timer — task. পর্ব ৩৩-এর Promise — তার then callback microtask. পর্ব ৩৪-এর async function — প্রতিটি await-এ function থেমে যায়, stack খালি করে দেয়, আর অপেক্ষার Promise পূর্ণ হলে বাকি অংশ একটি microtask হিসেবে ফিরে আসে। দেখতে sync কোডের মতো, কিন্তু ভেতরে সেই একই event loop. তাই await লিখলেই কাজ “পেছনে” চলে যায় না — await-এর আগের আর পরের ভারী হিসাব main thread-এই চলে।
পর্ব ৩৫-এর fetch-এ নেটওয়ার্কের অপেক্ষা করে ব্রাউজার; উত্তর এলে Promise পূর্ণ হয়, আর আপনার then বা await-এর পরের অংশ microtask হিসেবে চলে। আর পর্ব ৪২-এর Web Worker — নিজের আলাদা thread, নিজের আলাদা event loop.
চোখে দেখার টুল
- Loupe: Philip Roberts-এর তৈরি একটি ছোট ওয়েব টুল, যেখানে কোড লিখলে call stack, Web API আর callback queue-তে কী ঢুকছে-বেরোচ্ছে, অ্যানিমেশনে দেখা যায়। তিনি এটি দেখিয়েছিলেন JSConf EU 2014-এর বিখ্যাত বক্তৃতা “What the heck is the event loop anyway?”-তে — event loop বোঝার জন্য আজও সবচেয়ে বেশি দেখা ভিডিওগুলোর একটি। (Loupe microtask আলাদা দেখায় না; সেটুকু এই পর্বের ডায়াগ্রাম থেকে নিন।)
- DevTools Performance প্যানেল: Record চেপে পাতায় ক্লিক করুন; main thread-এর সারিতে প্রতিটি task একটি বাক্স, আর ৫০ ms-এর বেশি হলে কোণে লাল দাগ — long task. পর্ব ৫৩-এ বিস্তারিত।
- Node.js:
node --cpu-profদিয়ে profile নেওয়া যায়; আরperf_hooksmodule-এরmonitorEventLoopDelay()মাপে loop কতটা দেরিতে ঘুরছে — সার্ভার আটকে যাচ্ছে কি না বোঝার ভালো সূচক।
৯ · AI-এর যুগে — event loop নিয়ে AI-এর পরিচিত ভুল
Asynchronous কোড লেখায় AI দ্রুত, কিন্তু event loop-এর সূক্ষ্ম নিয়মে প্রায়ই ভুল করে — কারণ ইন্টারনেটে ভুল ব্যাখ্যাও প্রচুর। নিচের ভুলগুলো চিনে রাখুন:
| AI যা বলে বা লেখে | বাস্তবতা |
|---|---|
“setTimeout(fn, 0) সাথে সাথে চলে” | চলে চলমান কোড আর সব microtask-এর পরে, পরের কোনো task-এ; নেস্টিংয়ে অন্তত ৪ ms |
ভারী লুপে await Promise.resolve() দিয়ে “UI মুক্ত রাখা” | Microtask পাতা আঁকার আগেই চলে — কিছুই মুক্ত হয় না; task (setTimeout / scheduler.yield) লাগে |
Express handler-এ pbkdf2Sync, বড় JSON-এ লুপ, readFileSync | প্রতিটি request-এ পুরো সার্ভার থামে; async API, stream বা worker_threads ব্যবহার করুন |
সব জায়গায় process.nextTick — “এটা দ্রুততম” | বারবার nextTick I/O-কে না খাইয়ে রাখতে পারে; Node-এর ডক বেশিরভাগ ক্ষেত্রে queueMicrotask বা setImmediate সুপারিশ করে |
| মূল script-এ “setTimeout 0 সবসময় setImmediate-এর আগে” | নিশ্চিত নয়; শুধু I/O callback-এর ভেতরে setImmediate নিশ্চিতভাবে আগে |
“async লিখলেই function background thread-এ চলে” | একই main thread; শুধু await-এ থামে। আলাদা thread চাইলে Worker |
AI-কে async কোড লিখতে বা ক্রম ব্যাখ্যা করতে দিলে event loop-এর ভাষায় উত্তর চান — তাতে ভুল ধরা সহজ হয়:
AI-এর async কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১প্রায় সব GUI ব্যবস্থাই — শুধু ব্রাউজার নয় — UI চালায় একটি thread-এ। JavaScript-এর single thread কি দুর্বলতা, না একটি সচেতন নকশা?
১৯৯৫ সালে Brendan Eich যখন Netscape Navigator-এর জন্য JavaScript বানান, লক্ষ্য ছিল ওয়েব ডিজাইনারদের জন্য একটি সহজ “আঠা” ভাষা — ফর্ম যাচাই, ছবি বদলানো, বোতামে সাড়া। ব্রাউজার নিজেই তখন মূলত একটি thread-এ চলত। পাতার DOM একটি বিশাল, পরস্পর-জড়িত গাছ; দুটি thread যদি একসাথে সেটি বদলাতে পারত, প্রতিটি node-এ lock লাগত। আর lock মানেই deadlock, race condition — যে বাগ প্রতিবার একরকম ঘটে না, তাই খুঁজে পাওয়া কুখ্যাতভাবে কঠিন। শখের ওয়েব ডিজাইনারদের হাতে এমন হাতিয়ার দেওয়া অকল্পনীয় ছিল।
মজার ব্যাপার, এই সিদ্ধান্ত শুধু JavaScript-এর নয়। Windows-এর Win32-এ প্রতিটি window তার নিজের thread-এর message loop-এ বাঁধা; Java-র Swing-এ সব UI কাজ করতে হয় একটি “Event Dispatch Thread”-এ; Android-এ অন্য thread থেকে view বদলালে exception আসে; iOS-এ UIKit-এর কাজ main thread-এ করতে হয়। বহু দশক ধরে বহু দল multi-threaded UI toolkit বানানোর চেষ্টা করেছে, আর প্রায় সবাই শেষে এই উপসংহারে এসেছে: ইনপুট ঘটনা নিচ থেকে ওপরে যায় (মাউস → window → বোতাম), আর প্রোগ্রামের বদল ওপর থেকে নিচে নামে — দুই দিকের lock মিলে deadlock প্রায় অনিবার্য। তাই এক thread আর একটি ঘটনার লাইন — event loop — GUI-জগতের প্রায় সর্বজনীন সমাধান।
JavaScript এর সাথে যোগ করেছে run-to-completion নিশ্চয়তা: একটি callback শুরু হলে মাঝপথে অন্য JavaScript ঢুকবে না। ফলে if (stock > 0) stock--; লেখা নিরাপদ — দুটি ক্লিক একসাথে এসে স্টক −১ বানাতে পারে না (একটি async ধাপ মাঝে না থাকলে)। দামটা আমরা এই পর্বে দেখলাম: একটি লম্বা কাজ সব আটকায়। কিন্তু সেই দাম পরিষ্কার, চোখে দেখা যায়, মাপা যায় — আর ঠিক করার পথও পরিষ্কার: টুকরো করা, বা Worker.
মূল উপলব্ধি: single thread এখানে অক্ষমতা নয়, একটি নির্বাচিত সরলতা। জটিলতা (parallelism) পুরোপুরি বাদ দেওয়া হয়নি, শুধু আলাদা ও স্পষ্ট জায়গায় সরিয়ে রাখা হয়েছে — Worker, যারা ডেটা ভাগ না করে বার্তা পাঠায়।
প্র ০২Node.js কেন “প্রতি request-এ একটি thread” না নিয়ে event loop বেছে নিল? C10K সমস্যা কী ছিল?
১৯৯৯ সালে Dan Kegel একটি প্রবন্ধে প্রশ্ন তোলেন: একটি সাধারণ সার্ভার কি একসাথে দশ হাজার ক্লায়েন্ট সামলাতে পারে? এর নাম হয়ে যায় C10K সমস্যা। তখনকার প্রচলিত নকশা — Apache-এর মতো — প্রতিটি সংযোগের জন্য একটি process বা thread. প্রতিটি thread-এর নিজের stack (প্রায়ই কয়েকশো KB থেকে কয়েক MB), আর হাজার হাজার thread-এর মধ্যে অপারেটিং সিস্টেমকে বারবার context switch করতে হয়। অথচ বেশিরভাগ thread বেশিরভাগ সময় কিছুই করছে না — শুধু ধীর ক্লায়েন্ট বা ডেটাবেসের উত্তরের অপেক্ষায় বসে আছে।
বিকল্প ছিল evented I/O: একটি thread, যে অপারেটিং সিস্টেমকে জিজ্ঞেস করে “এই হাজার socket-এর মধ্যে কোনগুলো এখন প্রস্তুত?” — select, পরে আরও দক্ষ Linux-এর epoll আর BSD-এর kqueue. যেটি প্রস্তুত, শুধু সেটির কাজ করা হয়। nginx (২০০৪) এই পথেই Apache-কে অনেক ক্ষেত্রে ছাড়িয়ে যায়। সমস্যা ছিল প্রোগ্রামিং: C-তে callback-ভিত্তিক কোড লেখা কঠিন, আর Python বা Ruby-তে কোনো একটি library ভুল করে blocking কল করলেই পুরো loop আটকে যেত।
২০০৯ সালে JSConf EU-তে Ryan Dahl Node.js দেখান। তার যুক্তি ছিল: JavaScript-এর কোনো blocking I/O-এর ঐতিহ্যই নেই — ব্রাউজারে সবকিছু আগে থেকেই callback আর event দিয়ে চলে। তাই একটি সম্পূর্ণ ecosystem শুরু থেকেই non-blocking হতে পারে। V8 দিল গতি; নিচে প্রথমে libev/libeio, পরে Windows সমর্থনের জন্য নিজেদের তৈরি libuv. ফলে একটি Node process একটি thread-এ হাজার হাজার সংযোগ ধরে রাখতে পারে — chat, API gateway, real-time notification-এর জন্য আদর্শ।
কিন্তু দুর্বলতাও এখানেই: কাজ যদি CPU-ভারী হয় (ভিডিও encode, বড় হিসাব), event loop কোনো সাহায্য করে না, বরং সবাইকে আটকায়। তাই Node-এ এসেছে worker_threads আর cluster. আর thread-জগৎও থেমে নেই — Java-র virtual thread বা Go-র goroutine হালকা thread দিয়ে C10K-কে “sync দেখতে কোড”-এ সমাধান করে।
মূল উপলব্ধি: event loop অপেক্ষা-প্রধান (I/O-bound) কাজে দারুণ দক্ষ, গণনা-প্রধান (CPU-bound) কাজে নয়। কোনো সার্ভারের নকশা বেছে নেওয়ার আগে জিজ্ঞেস করুন: আমার request-গুলো বেশিরভাগ সময় অপেক্ষা করে, না হিসাব করে?
প্র ০৩Task তো ছিলই — তাহলে microtask নামের আলাদা লাইন কেন বানাতে হলো? HTML Standard আর ECMAScript এখানে কীভাবে ভাগাভাগি করে?
ব্রাউজারের event loop প্রথম দিকে কোনো লিখিত মানদণ্ডে ছিল না — প্রতিটি ব্রাউজার নিজের মতো করত। HTML5-এর কাজ চলার সময় (WHATWG-এর HTML Standard, যা এখন একটি “living standard”) প্রথমবার বিস্তারিতভাবে লেখা হয়: event loop কী, task queue কী, কখন rendering-এর সুযোগ আসে। ECMAScript নিজে কোনো event loop সংজ্ঞায়িত করে না; ES2015 শুধু বলে Promise-এর callback একটি “job” হিসেবে queue-তে যাবে, আর host (ব্রাউজার বা Node) ঠিক করবে কখন চালাবে। ব্রাউজারে এই job-গুলোই microtask.
Microtask-এর প্রথম বড় প্রয়োজন এসেছিল DOM থেকে। পুরোনো “mutation event” (যেমন DOMNodeInserted) প্রতিটি বদলের সাথে সাথে synchronous চলত — একটি লুপে হাজারটা node যোগ করলে হাজারবার handler, আর handler নিজে DOM বদলালে বিশৃঙ্খলা। এর বদলে আসে MutationObserver: বদলগুলো জমা হয়, আর চলমান script শেষ হওয়ার পরে, কিন্তু পাতা আঁকা বা পরের task-এর আগে, একসাথে একবার খবর দেয়। এই “শেষ হলেই, কিন্তু অন্য কিছুর আগে” সময়টাই microtask checkpoint.
Promise-এরও ঠিক এই সময়টাই দরকার ছিল। Promise-জগতে একটি গুরুত্বপূর্ণ নিয়ম: then-এর callback সবসময় asynchronous চলবে, Promise আগেই পূর্ণ হয়ে থাকলেও। কেন? কোনো API যদি কখনো sync, কখনো async callback ডাকে, তার আচরণ অনুমান করা অসম্ভব — Node-এর Isaac Schlueter ২০১৩ সালের একটি লেখায় এই বিপদকে মজা করে “Zalgo ছেড়ে দেওয়া” বলেছিলেন। কিন্তু প্রতিটি then-কে একটি পুরো task বানালে মাঝে rendering আর অন্য task ঢুকে যেত, আর ৪ ms clamp-এর মতো দেরি জমত। Microtask দুটোর মাঝামাঝি: সবসময় async, কিন্তু যত দ্রুত সম্ভব।
পরে (২০১৮–১৯ সালের দিকে ব্রাউজারগুলোতে) সরাসরি microtask যোগের জন্য queueMicrotask আসে। আর await যেহেতু Promise-এর ওপর তৈরি, async function-এর প্রতিটি ধাপও microtask.
মূল উপলব্ধি: ভাষা (ECMAScript) আর host (HTML/Node) কাজ ভাগ করে নেয় — ভাষা বলে “কী” queue-তে যাবে, host বলে “কখন” চলবে। microtask হলো “এখনকার কাজের পরিণতি” সামলানোর লাইন, আর task হলো “বাইরের দুনিয়া থেকে নতুন ঘটনা”-র লাইন।
প্র ০৪২০২৪ সালে Core Web Vitals-এ FID সরিয়ে INP আনা হলো। কেন? আর এটি event loop-এর সাথে কীভাবে জড়িত?
২০২০ সালে Google যখন Core Web Vitals ঘোষণা করে, তিনটি মাপ ছিল: লোড হওয়ার গতি (LCP), লেআউট নড়াচড়া (CLS), আর সাড়া দেওয়ার গতি — FID (First Input Delay). FID মাপত শুধু পাতার প্রথম ইন্টারঅ্যাকশনের input delay — অর্থাৎ main thread অন্য কাজে ব্যস্ত ছিল বলে আপনার handler শুরু হতে কত দেরি হলো। Handler নিজে কত সময় নিল, বা পর্দা আঁকতে কত লাগল — সেগুলো ধরত না।
ফলে বাস্তবে বেশিরভাগ সাইট FID-এ “ভালো” পেত, অথচ ব্যবহারকারীরা ধীর মেনু, আটকে যাওয়া “কার্টে যোগ করুন” বোতামের অভিযোগ করতেন। প্রথম ক্লিক পাতা লোডের সময়, পরের শত ক্লিক বাদ; আর অংশ ২-এর মতো ৮০০ ms-এর handler-এর FID শূন্যের কাছাকাছিও হতে পারত, যদি ক্লিকের মুহূর্তে main thread খালি থাকে।
Chrome দল Event Timing API-এর ওপর ভিত্তি করে নতুন মাপ বানায় — INP (Interaction to Next Paint). এটি পাতায় থাকার পুরো সময়ের প্রায় সব ক্লিক, ট্যাপ আর কী-চাপ দেখে, আর প্রতিটির জন্য মাপে input delay + handler-এর সময় + পরের ফ্রেম আঁকা পর্যন্ত দেরি। রিপোর্ট করে প্রায় সবচেয়ে খারাপটি (অনেক ইন্টারঅ্যাকশন হলে একেবারে চরম কয়েকটি বাদ দিয়ে)। কয়েক বছর পরীক্ষামূলক থাকার পর ২০২৪ সালের মার্চে INP আনুষ্ঠানিকভাবে FID-এর জায়গা নেয়; ভালো হওয়ার সীমা ২০০ ms.
Event loop-এর ভাষায় INP হলো: “ইনপুট task queue-তে আসা থেকে পরের rendering ধাপ পর্যন্ত সময়”। তিনটি শত্রু স্পষ্ট — ইনপুটের আগে চলা long task (input delay), ভারী handler (processing), আর বিশাল DOM বদল (presentation). সমাধানও এই পর্বেরই: handler-এ আগে দৃশ্যমান সাড়া দিয়ে yield করা, বাকি কাজ পরের task-এ বা Worker-এ পাঠানো। এই কারণেই scheduler.yield()-এর মতো API এসেছে।
মূল উপলব্ধি: যা মাপা হয়, তাই উন্নত হয় — আর ভুল জিনিস মাপলে ভুল জিনিস উন্নত হয়। FID সহজে “ভালো” দেখাত; INP ব্যবহারকারীর আসল অভিজ্ঞতার কাছাকাছি, আর সরাসরি event loop-এর স্বাস্থ্য দেখায়।
প্র ০৫Classic Mac OS আর Windows 3.x-এ একটি অ্যাপ আটকে গেলে পুরো কম্পিউটার জমে যেত। JavaScript-এর event loop কি সেই পুরোনো “cooperative multitasking”-এর ফিরে আসা?
অনেকটাই, হ্যাঁ। ১৯৮০-৯০-এর দশকে Classic Mac OS (MultiFinder থেকে Mac OS 9 পর্যন্ত) আর Windows 3.x-এর Windows অ্যাপগুলো চলত cooperative multitasking-এ: প্রতিটি অ্যাপ নিজের কাজ খানিকটা করে স্বেচ্ছায় নিয়ন্ত্রণ ফিরিয়ে দিত (event loop-এ ফিরে গিয়ে পরের ঘটনা চাইত), তবেই অন্য অ্যাপ চলার সুযোগ পেত। কোনো অ্যাপ বাগের কারণে অসীম লুপে পড়লে বা লম্বা হিসাবে আটকালে আর কেউ চলতে পারত না — মাউসের কার্সর জমে যেত, অনেক সময় restart ছাড়া উপায় থাকত না।
এর সমাধান ছিল preemptive multitasking: অপারেটিং সিস্টেম নিজেই একটি timer interrupt দিয়ে কয়েক মিলিসেকেন্ড পরপর চলমান প্রোগ্রামকে থামিয়ে অন্যকে সুযোগ দেয় — অ্যাপের অনুমতি ছাড়াই। Unix-এ এটি শুরু থেকেই ছিল; Windows 95/NT-তে ৩২-বিট অ্যাপের জন্য, আর Mac-এ ২০০১ সালের Mac OS X থেকে। আজকের সব অপারেটিং সিস্টেম preemptive.
এবার JavaScript-এর main thread দেখুন: প্রতিটি callback যতক্ষণ চায় চলে, কেউ তাকে মাঝপথে থামায় না (run-to-completion); অন্যরা সুযোগ পায় কেবল সে স্বেচ্ছায় শেষ হলে বা await/yield করলে। হুবহু cooperative multitasking — একটি পাতার ভেতরে। আর পরিণতিও একই: একটি খারাপ callback পুরো পাতা জমিয়ে দেয়। তফাত হলো, এখন ক্ষতির সীমা একটি tab; ব্রাউজার আর অপারেটিং সিস্টেম নিজেরা preemptive, তাই বাকি সব চলতে থাকে, আর ব্রাউজার “Page unresponsive” দেখিয়ে আটকে থাকা পাতা বন্ধ করার সুযোগ দেয়।
তাহলে কেন এই “পুরোনো” মডেল? কারণ একটি প্রোগ্রামের ভেতরে cooperative মডেলের বিরাট সুবিধা আছে: কোড কোথায় থামতে পারে তা আপনি জানেন (শুধু await-এ বা callback-এর সীমানায়), তাই race condition বোঝা সহজ, lock লাগে না। Python asyncio, Go-র শুরুর দিকের scheduler, Kotlin coroutine — সবাই কোনো না কোনো রূপে এই চিন্তা ব্যবহার করে। scheduler.yield() আসলে সেই পুরোনো Mac প্রোগ্রামারদের পরামর্শেরই আধুনিক রূপ: “লম্বা কাজে মাঝে মাঝে নিয়ন্ত্রণ ফিরিয়ে দিন।”
মূল উপলব্ধি: কম্পিউটিংয়ের ইতিহাসে নকশা ফিরে ফিরে আসে, নতুন জায়গায়। Cooperative মডেল সরলতা দেয়, বিনিময়ে দায়িত্ব দেয় প্রোগ্রামারকে — main thread-এর ভাগ সবার, আর যে বেশি নেয়, সে সবার অভিজ্ঞতা নষ্ট করে।
অনুশীলন
প্রথমে খাতায় অনুমান, তারপর চালিয়ে মিলিয়ে দেখা — event loop শেখার সবচেয়ে ভালো উপায়।
-
ক্রম অনুমান করুন: খাতায় তিন কলাম এঁকে আউটপুটের ক্রম লিখুন, তারপর ব্রাউজারের console-এ চালান:
setTimeout(() => console.log(1)); Promise.resolve().then(() => { console.log(2); setTimeout(() => console.log(3)); queueMicrotask(() => console.log(4)); }); console.log(5);ক্রম:
5,2,4,1,3. Script-এ sync শুধু5. Microtask কলামে then-এর callback; সেটি চলে2ছাপে, task কলামে timer ৩ যোগ করে (timer ১-এর পেছনে), আর microtask কলামে ৪ যোগ করে। Microtask খালি না হওয়া পর্যন্ত চলে — তাই4. তারপর task: আগে timer ১ (আগে লাইনে দাঁড়িয়েছিল), পরে timer ৩. (delay না দিলে ০ ধরা হয়।) -
বাগ খুঁজুন: একজন সহকর্মী পাতা জমে যাওয়া ঠেকাতে লিখেছেন
for (const row of rows) { render(row); await Promise.resolve(); }— তবু ১০,০০০ সারিতে পাতা কয়েক সেকেন্ড জমে থাকে, আর লোডিং স্পিনারও ঘোরে না। কেন? কীভাবে ঠিক করবেন?await Promise.resolve()-এর পরের অংশ একটি microtask. Event loop microtask queue খালি না করে rendering বা অন্য task-এ যায় না, আর প্রতিটি ধাপ পরের microtask যোগ করছে — ফলে পুরো লুপ একটানা চলে, যেন কোনোawait-ই নেই। ঠিক করতে প্রতি কিছু সময় পরপর (যেমন ~৪০ ms) একটি task-এ যেতে হবে:await new Promise(r => setTimeout(r, 0)), বা যেখানে আছে সেখানেawait scheduler.yield(). প্রতিটি সারির পরে নয় — তাতে মোট সময় অনেক বেড়ে যাবে (৪ ms clamp মনে করুন)। আরও ভালো সমাধান হতে পারে একসাথে ১০,০০০ সারি না এঁকে শুধু পর্দায় দেখা অংশ আঁকা (virtual list). -
Node-এর phase চিনুন: একটি Node.js (CommonJS) প্রোগ্রামে
fs.readFile-এর callback-এর ভেতরে এই চারটি লেখা আছে, এই ক্রমে:setTimeout(a, 0),setImmediate(b),Promise.resolve().then(c),process.nextTick(d). কোনটি কখন চলবে, আর কেন এখানে ক্রম নিশ্চিত?ক্রম:
d→c→b→a. readFile-এর callback চলে poll phase-এ। Callback শেষ হতেই আগে nextTick queue (d), তারপর promise microtask (c). এরপর loop poll থেকে সরাসরি check phase-এ যায় — setImmediate (b). Timer (a)-কে অপেক্ষা করতে হয় পরের ঘুরের timers phase পর্যন্ত। ক্রম নিশ্চিত, কারণ poll-এর পরে check আসবেই, timers-এর আগে। মূল script-এ (I/O-এর বাইরে) কিন্তু a আর b-এর ক্রম নিশ্চিত নয়। -
Long task নিজের চোখে দেখুন: যেকোনো পাতায় DevTools খুলে Performance প্যানেলে Record চাপুন, তারপর console-এ চালান
const end = Date.now() + 300; while (Date.now() < end) {}, এরপর Record থামান। Main সারিতে কী দেখলেন? একই কাজ ১০টি ৩০ ms টুকরোয় ভাঙলে (প্রতিটির মাঝেsetTimeout) ছবিটা কেমন হতো?Main সারিতে প্রায় ৩০০ ms চওড়া একটি ধূসর “Task” বাক্স দেখবেন, কোণে লাল ত্রিভুজ বা লাল দাগ — ৫০ ms ছাড়ানো long task-এর চিহ্ন; বাক্সে মাউস রাখলে long task-এর সতর্কতা দেখায়। ওই সময়ে কোনো frame আঁকা হয়নি। ১০টি ৩০ ms টুকরোয় ভাঙলে দেখবেন ১০টি সরু বাক্স, প্রতিটি ৫০ ms-এর নিচে, কোনো লাল দাগ নেই, আর ফাঁকে ফাঁকে frame আঁকা ও ইনপুট সামলানোর জায়গা। মোট CPU কাজ একই (বরং সামান্য বেশি), কিন্তু ব্যবহারকারীর অভিজ্ঞতা সম্পূর্ণ আলাদা — এটাই chunking-এর মূল কথা।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমব্রাউজারের event loop-এর এক ঘুর সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📥 Task queue থেকে সবচেয়ে পুরোনো একটি task নেওয়া
- ⚙️ সেই task শেষ পর্যন্ত চালানো (run-to-completion)
- ⚡ Microtask queue পুরো খালি করা
- 🎞️ আঁকার সময় হলে requestAnimationFrame callback চালানো
- 🎨 Style, layout আর paint — পর্দা আঁকা
JavaScript-এ console.log("x"), setTimeout(() => console.log("y"), 0) আর queueMicrotask(() => console.log("z")) — এই ক্রমে লেখা থাকলে আউটপুট কী?
- x, y, z
- z, x, y
- x, z, y
- y, x, z
একটি microtask নিজেকে বারবার queueMicrotask দিয়ে আবার নির্ধারণ করলে ব্রাউজারে কী হয়?
- প্রতিটি microtask-এর ফাঁকে পাতা আঁকা হয়, তাই সমস্যা নেই
- Microtask queue কখনো খালি হয় না, পাতা জমে যায় আর ক্লিক সাড়া পায় না
- কয়েকবার পরে RangeError: Maximum call stack size exceeded আসে
- ব্রাউজার নিজে থেকেই প্রতিটি microtask-কে ৪ ms দেরির task-এ বদলে দেয়
একটি ভারী লুপ চলার সময় পাতাকে ক্লিক সামলানো আর আঁকার সুযোগ দিতে লুপের মাঝে কোনটি লেখা উচিত?
- await Promise.resolve()
- queueMicrotask(next)
- process.nextTick(next)
- await new Promise(r => setTimeout(r, 0))
Node.js (CommonJS)-এর মূল script-এ Promise.resolve().then(p) আর তারপর process.nextTick(n) লেখা হলো। কোনটি আগে চলবে?
- n, তারপর p
- p, তারপর n
- ক্রম অনিশ্চিত — প্রতিবার আলাদা
- দুটিই পরের ঘুরের timers phase-এ চলবে
Core Web Vitals-এর INP (Interaction to Next Paint) মূলত কী মাপে?
- পাতার সবচেয়ে বড় ছবি বা লেখা কত দ্রুত দেখা যায়
- শুধু প্রথম ইন্টারঅ্যাকশনে handler শুরু হতে কত দেরি হলো
- ক্লিক, ট্যাপ বা কী-চাপ থেকে পরের বার পর্দা আঁকা পর্যন্ত দেরি — প্রায় সবচেয়ে ধীর ইন্টারঅ্যাকশনটির
- পাতা লোডের সময় লেআউট কতটা সরে যায়
ব্রাউজারে একটি async function ডাকা হলো, যার প্রথম লাইনে console.log("s") আর তারপর await null;. Function ডাকার ঠিক পরের লাইনে আছে console.log("after"). আউটপুটের প্রথম দুটি লাইন কী?
- after, তারপর s
- s, তারপর after
- শুধু after — s ছাপা হয় না কারণ async function background-এ চলে
- ক্রম ব্রাউজারভেদে আলাদা হয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- JavaScript চলে একটি thread-এ; অপেক্ষার কাজ করে runtime (Web API / libuv), আর শেষ হলে callback queue-তে দাঁড়ায়।
- Event loop-এর নিয়ম: একটি task → সব microtask → (দরকার হলে) rAF ও পাতা আঁকা → আবার একটি task. প্রতিটি callback শেষ পর্যন্ত চলে (run-to-completion).
- Microtask: Promise-এর then,
await-এর পরের অংশ,queueMicrotask. Task: timer, event, message.setTimeout(fn, 0)মানে “পরের কোনো task-এ”, এখনই নয়। - Long task (৫০ ms-এর বেশি) ক্লিক আর আঁকা আটকায়, INP খারাপ করে। কাজ টুকরো করে task দিয়ে yield করুন (setTimeout / scheduler.yield), microtask দিয়ে নয়; ভারী হিসাব Worker-এ।
- Node.js: phase — timers → pending → poll → check → close; প্রতিটি callback-এর পরে আগে
nextTick, তারপর promise. Request handler-এ কখনো CPU-ভারী sync কাজ নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩২ · callback, setTimeout আর callback hellপরবর্তী পর্বTask queue-এর callback নিজের হাতে লেখা — timer, interval আর তাদের ফাঁদ।
- JavaScript প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python-এর asyncioতুলনা করুনএকই event loop-এর চিন্তা, Python-এর coroutine দিয়ে।
- Java-র virtual threadতুলনা করুনউল্টো পথ: প্রতি কাজে একটি হালকা thread, আর sync দেখতে কোড।
- C++-এ নেটওয়ার্কিংগভীরেsocket আর select/epoll — libuv-এর নিচে যা আছে।