JavaScript ইভেন্ট লুপ যা আপনার ওয়েব পেজ পরিচালনা করে, তা একটি Node.js সার্ভার পরিচালনাকারী ইভেন্ট লুপ থেকে সম্পূর্ণ আলাদা। আপনি যদি সতর্ক না হন, তবে এই অমিল UI ফ্রিজ করে দিতে পারে বা I/O ব্লক করে দিতে পারে। যারা উভয় পরিবেশে চলে এমন async কোড লেখেন, তাদের জন্য এই দুটি লুপের পার্থক্য জানা অত্যন্ত জরুরি।

কেন এই পার্থক্যটি গুরুত্বপূর্ণ

ইভেন্ট লুপ ECMAScript স্পেসিফিকেশন দ্বারা সংজ্ঞায়িত নয়; এটি হোস্ট (host) দ্বারা নির্ধারিত হয়। ব্রাউজারকে ফ্রেম রেন্ডার করার সময় পেজটিকে রেসপন্সিভ রাখতে হয়, অন্যদিকে Node.js তৈরি করা হয়েছে নন-ব্লকিং I/O-এর ওপর ভিত্তি করে। একটি হোস্টের প্যাটার্ন অন্যটিতে ব্যবহার করলে এমন বাগ তৈরি হতে পারে যা খুঁজে বের করা কঠিন: একটি দীর্ঘ প্রমিজ (promise) চেইন ব্রাউজারের রিপেইন্ট (repaint) আটকে দিতে পারে, আবার একটি অনিয়ন্ত্রিত process.nextTick লুপ Node-কে তার I/O ফেজে পৌঁছাতে বাধা দিতে পারে।

ব্রাউজারের টার্ন-বেসড লুপ

ব্রাউজারে, লুপটি একটি একক চক্র (cycle) চালায় যা টাস্ক এক্সিকিউশন, মাইক্রোটাস্ক ড্রেনিং এবং রেন্ডারিং-এর মধ্যে সমন্বয় করে:

  1. একটি ম্যাক্রোটাস্ক চালান (একটি ক্লিক হ্যান্ডলার, একটি setTimeout, ইত্যাদি)।
  2. সবগুলো মাইক্রোটাস্ক শেষ করুন (promises, queueMicrotask)।
  3. যদি একটি ফ্রেমের সময় হয়ে থাকে, তবে টার্গেট 60 fps বজায় রাখতে পেইন্ট (paint) এবং কম্পোজিট (composite) করুন।
  4. পুনরাবৃত্তি করুন।

দুটি API ডেভেলপারদের এই চক্রে সরাসরি যুক্ত হওয়ার সুযোগ দেয়:

  • requestAnimationFrame – ব্রাউজার পেইন্ট করার ঠিক আগে এটি কল করা হয়। অ্যানিমেশনের কাজের জন্য এটি সঠিক জায়গা কারণ কলব্যাকটি বর্তমান মাইক্রোটাস্কগুলোর পরে কিন্তু পরবর্তী ফ্রেমের আগে চলে।
  • requestIdleCallback – যখন ব্রাউজারের কোনো উচ্চ-অগ্রাধিকারের (high-priority) কাজ থাকে না, তখন এটি কল করা হয়। অ্যানালিটিক্স বা ডেটা প্রি-লোডিংয়ের মতো কম গুরুত্বপূর্ণ কাজের জন্য এটি উপযোগী।

ফাঁদ: মাইক্রোটাস্ক স্টারভেশন

যেহেতু ব্রাউজার রেন্ডার করার আগে মাইক্রোটাস্ক কিউ (queue) খালি করে ফেলে, তাই একটি দীর্ঘ প্রমিজ চেইন UI-কে রেন্ডার হতে বাধা দিতে পারে। কল স্ট্যাক ব্লক হয় না; পেজটি কেবল রেন্ডার ধাপে পৌঁছাতে পারে না, যা ব্যবহারকারীর কাছে একটি ফ্রিজ বা হ্যাং হওয়ার মতো মনে হয়।

Node-এর libuv-চালিত লুপ

Node.js তার লুপটি libuv-এর ওপর ন্যস্ত করে, যা একটি C লাইব্রেরি এবং এটি কাজগুলোকে বিভিন্ন ফেজে বিভক্ত করে, যার প্রতিটির নিজস্ব কিউ থাকে:

  1. TimerssetTimeout এবং setInterval থেকে আসা কলব্যাক।
  2. Pending callbacks – বিলম্বিত I/O কলব্যাক যা ইতিমধ্যে OS লেভেলে সম্পন্ন হয়েছে।
  3. Poll – নতুন I/O ইভেন্ট সংগ্রহ করে (ফাইল রিড, নেটওয়ার্ক ডেটা)।
  4. ChecksetImmediate কলব্যাক চালায়।
  5. Close callbacks – যখন কোনো সকেট বা হ্যান্ডেল বন্ধ হয় তখন এটি কার্যকর হয়।

দুটি কনস্ট্রাক্ট এই ফেজ অর্ডারের বাইরে থাকে:

  • process.nextTick – বর্তমান অপারেশন শেষ হওয়ার ঠিক পরেই মাইক্রোটাস্ক কিউ-এর আগে এটি চলে।

ফাঁদ: I/O স্টারভেশন

যদি একটি ফাংশন বিরতি না দিয়ে বারবার process.nextTick শিডিউল করে, তবে Node কখনোই “next-tick” ধাপের বাইরে যেতে পারে না। নেটওয়ার্ক রিকোয়েস্ট, ফাইল রিড এবং টাইমারগুলো অলস বসে থাকে, যার ফলে সার্ভার-সাইড ল্যাটেন্সি বৃদ্ধি পায় বা সার্ভার পুরোপুরি হ্যাং হয়ে যেতে পারে।

ব্যবহারিক ক্ষেত্রে setImmediate বনাম setTimeout

উভয়ই পরবর্তী ইটারেশনের জন্য কলব্যাক শিডিউল করে, তবে তাদের আপেক্ষিক ক্রম নির্ভর করে তারা কোথায় কল করা হয়েছে তার ওপর:

  • টপ-লেভেল কোড – ক্রম নিশ্চিত নয়; এটি প্রসেসটি কত দ্রুত শুরু হচ্ছে তার ওপর নির্ভর করে।
  • একটি I/O কলব্যাকের ভেতরে – ক্রমটি সুনির্দিষ্ট (deterministic): setImmediate, setTimeout(fn, 0)-এর আগে চলে। Poll ফেজ শেষ হওয়ার পরে, libuv পুনরায় টাইমার ফেজে প্রবেশ করার আগে Check ফেজে (যেখানে setImmediate থাকে) চলে যায়।

যখন আপনি কোনো রিসোর্স রিড শেষ হওয়ার ঠিক পরেই তা ক্লিনআপ করার মতো সুনির্দিষ্ট সিকোয়েন্সের ওপর নির্ভর করেন, তখন এই সূক্ষ্ম পার্থক্যটি গুরুত্বপূর্ণ হয়ে ওঠে।

এক নজরে মূল পার্থক্যসমূহ

  • লক্ষ্য (Goal): ব্রাউজার ভিজ্যুয়াল আপডেটের ওপর অগ্রাধিকার দেয়; Node I/O প্রস্তুতির ওপর অগ্রাধিকার দেয়।
  • রেন্ডারিং হুক (Rendering hook): requestAnimationFrame (শুধুমাত্র ব্রাউজারে)।
  • ফেজ-নির্দিষ্ট হুক (Phase-specific hook): setImmediate (শুধুমাত্র Node-এ, Check ফেজে চলে)।
  • উচ্চ-অগ্রাধিকার কিউ (High-priority queue): process.nextTick (শুধুমাত্র Node-এ, মাইক্রোটাস্কের আগে চলে)।
  • স্টারভেশনের ঝুঁকি (Starvation risk): ব্রাউজারে দীর্ঘ প্রমিজ চেইন; Node-এ সীমাহীন process.nextTick

পরবর্তী করণীয়

আপনি যদি এমন কোনো কোডবেস মেইনটেইন করেন যা উভয় পরিবেশে চলে (যেমন, isomorphic libraries), তবে নিচের বিষয়গুলো পরীক্ষা করুন:

  • ইভেন্ট লুপে বিরতি না দিয়ে অনেকগুলো প্রমিজ চেইন ব্যবহার করা হচ্ছে কি না। রেন্ডারারকে সুযোগ দিতে await new Promise(r => setTimeout(r, 0)) ব্যবহার করুন অথবা ব্রাউজারে requestIdleCallback ব্যবহার করুন।
  • যে কাজগুলো পরে করা সম্ভব তার জন্য process.nextTick ব্যবহার করা হচ্ছে কি না। যখন “next-tick” জরুরি না, তখন setImmediate বা সাধারণ প্রমিজ ব্যবহার করা শ্রেয়।
  • setTimeout(fn, 0) এবং setImmediate যে একই কাজ করে তা ধরে নেওয়া। যদি সিকোয়েন্স গুরুত্বপূর্ণ হয়, তবে I/O কলব্যাকের ভেতরে তাদের ক্রম পরীক্ষা করে দেখুন।

সারসংক্ষেপ

ইভেন্ট লুপ হলো একটি হোস্ট-নির্দিষ্ট শিডিউলার, এটি জাভাস্ক্রিপ্টের কোনো সার্বজনীন বৈশিষ্ট্য নয়। ব্রাউজার রেন্ডারিং-কে লুপের সাথে সমন্বয় করে; অন্যদিকে Node I/O-কে libuv ফেজগুলোতে আলাদা করে রাখে। প্রায়োরিটি মেকানিজম—যেমন ব্রাউজারে microtasks বা Node-এ process.nextTick—এর অপব্যবহার সিস্টেমের সেই অংশটিকে অচল করে দিতে পারে যা প্রতিটি এনভায়রনমেন্ট সেবা করার জন্য তৈরি করা হয়েছে। আপনার async প্যাটার্নগুলোকে হোস্টের লুপ মডেলের সাথে সামঞ্জস্যপূর্ণ করুন, তাহলে আপনি ফ্রোজেন পেজ এবং ব্লকড সার্ভার—উভয় সমস্যাই এড়াতে পারবেন।