तुमच्या वेब पेजला चालवणारा JavaScript event loop आणि Node.js सर्व्हरला चालवणारा event loop यामध्ये खूप फरक असतो, आणि जर तुम्ही सावध नसाल तर हा फरक UI फ्रीझ करू शकतो किंवा I/O मध्ये अडथळा आणू शकतो. जे लोक दोन्ही वातावरणात चालणारा async कोड लिहितात, त्यांच्यासाठी हे दोन्ही कुठे वेगळे होतात हे जाणून घेणे आवश्यक आहे.

हा फरक का महत्त्वाचा आहे

event loop ही ECMAScript spec द्वारे परिभाषित केलेली गोष्ट नाही; ती 'host' मध्ये असते. ब्राउझर्सना फ्रेम्स रेंडर करताना पेज रिस्पॉन्सिव्ह ठेवावे लागते, तर Node.js हे non-blocking I/O वर आधारित आहे. एका host मध्ये काम करणारे पॅटर्न दुसऱ्या host मध्ये वापरल्यास असे बग्स येऊ शकतात जे शोधणे कठीण असते: promises ची एक लांब साखळी ब्राउझरचे repaint थांबवू शकते, तर अनियंत्रित process.nextTick लूप Node ला त्याच्या I/O फेजेसपर्यंत पोहोचण्यापासून रोखू शकतो.

ब्राउझरचा टर्न-आधारित (turn-based) लूप

ब्राउझरमध्ये, लूप एक सिंगल सायकल चालवते ज्यामध्ये टास्क एक्झिक्यूशन, microtask ड्रेनिंग आणि रेंडरिंग यांचा मेळ घातला जातो:

  1. एक macrotask चालवा (उदा. click handler, setTimeout, इ.).
  2. सर्व microtasks ड्रेन करा (promises, queueMicrotask).
  3. जर एखादी फ्रेम रेंडर करण्याची वेळ आली असेल, तर लक्ष्यित 60 fps गाठण्यासाठी पेंट आणि कंपोझ करा.
  4. पुन्हा करा.

दोन APIs डेव्हलपर्सना या सायकलमध्ये स्पष्ट 'hooks' देतात:

  • requestAnimationFrame – ब्राउझर पेंट करण्याच्या अगदी आधी कॉल केला जातो. ॲनिमेशनच्या कामासाठी ही योग्य जागा आहे कारण हे callback सध्याच्या microtasks नंतर परंतु पुढच्या फ्रेमच्या आधी चालते.
  • requestIdleCallback – जेव्हा ब्राउझरकडे कोणतेही हाय-प्रायोरिटी काम नसते तेव्हा हे कार्यान्वित होते. ॲनालिटिक्स किंवा डेटा प्री-लोडिंग सारख्या कमी प्रभावाच्या कामांसाठी हे उपयुक्त आहे.

अडथळा (Pitfall): microtask starvation

ब्राउझर रेंडर करण्यापूर्वी microtask queue रिकामी करत असल्यामुळे, promises ची एक लांब साखळी UI ला कधीही पेंट होऊ देणार नाही. यामध्ये कॉल स्टॅक ब्लॉक होत नाही; पेज फक्त रेंडरिंग स्टेपपर्यंत कधीच पोहोचत नाही, ज्यामुळे वापरकर्त्याला पेज फ्रीझ झाल्यासारखे वाटते.

Node चा libuv-driven लूप

Node.js त्याचा लूप libuv कडे सोपवते, जी एक C लायब्ररी आहे आणि ती कामाचे वेगवेगळ्या टप्प्यांमध्ये (phases) विभाजन करते, ज्यातील प्रत्येकाची स्वतःची क्यू (queue) असते:

  1. TimerssetTimeout आणि setInterval मधून येणारे callbacks.
  2. Pending callbacks – OS लेव्हलवर आधीच पूर्ण झालेले प्रलंबित (deferred) I/O callbacks.
  3. Poll – नवीन I/O इव्हेंट्स मिळवणे (file reads, network data).
  4. ChecksetImmediate callbacks चालवणे.
  5. Close callbacks – जेव्हा एखादा socket किंवा handle बंद होतो तेव्हा कार्यान्वित होतात.

दोन कन्स्ट्रक्ट्स या फेज ऑर्डरच्या बाहेर असतात:

  • process.nextTick – सध्याचे ऑपरेशन संपल्यानंतर लगेच, microtask queue च्या आधी चालते.

अडथळा (Pitfall): I/O starvation

जर एखादे फंक्शन 'yield' न करता वारंवार process.nextTick शेड्यूल करत असेल, तर Node कधीही "next-tick" स्टेपच्या पुढे जाऊ शकत नाही. यामुळे नेटवर्क रिक्वेस्ट, फाईल रीड्स आणि टाइमर्स थांबतात, ज्यामुळे सर्व्हर-साइड लेटन्सी वाढते किंवा सर्व्हर पूर्णपणे हँग होऊ शकतो.

प्रत्यक्ष वापरात setImmediate विरुद्ध setTimeout

दोन्ही पुढच्या इटरेशनसाठी callbacks शेड्यूल करतात, परंतु त्यांचा क्रम ते कुठे कॉल केले आहेत यावर अवलंबून असतो:

  • Top-level code – यामध्ये क्रम निश्चित नसतो; तो प्रोसेस किती वेगाने सुरू होते यावर अवलंबून असतो.
  • Inside an I/O callback – यामध्ये क्रम निश्चित (deterministic) असतो: setImmediate हे setTimeout(fn, 0) च्या आधी चालते. Poll फेज संपल्यानंतर, libuv झिरो-डिले टाइमआउटसाठी पुन्हा Timers फेजमध्ये जाण्यापूर्वी Check फेजमध्ये (जिथे setImmediate असते) जाते.

जेव्हा तुम्हाला अचूक सिक्वेन्सिंगची गरज असते, जसे की एखादी फाईल वाचून झाल्यावर लगेच रिसोर्स क्लीनअप करणे, तेव्हा ही सूक्ष्मता महत्त्वाची ठरते.

मुख्य फरक एका दृष्टीक्षेपात

  • ध्येय (Goal): ब्राउझर्स व्हिज्युअल अपडेट्सना प्राधान्य देतात; Node I/O रेडीनेसला प्राधान्य देते.
  • रेंडरिंग हुक: requestAnimationFrame (फक्त ब्राउझर).
  • फेज-विशिष्ट हुक: setImmediate (फक्त Node, Check फेजमध्ये चालते).
  • हाय-प्रायोरिटी क्यू: process.nextTick (फक्त Node, microtasks च्या आधी चालते).
  • Starvation चा धोका: ब्राउझरमध्ये लांब promise साखळ्या; Node मध्ये अमर्याद process.nextTick.

पुढे काय पाहावे (किंवा काय काळजी घ्यावी)

जर तुम्ही अशा कोडबेसवर काम करत असाल जो दोन्ही वातावरणात चालतो (उदा. isomorphic libraries), तर खालील गोष्टी तपासा:

  • इव्हेंट लूपला 'yield' न करता अनेक promises साखळीने जोडू नका. await new Promise(r => setTimeout(r, 0)) वापरा किंवा ब्राउझरमध्ये रेंडररला संधी देण्यासाठी requestIdleCallback वापरा.
  • जे काम पुढे ढकलले जाऊ शकते त्यासाठी process.nextTick वापरू नका. जेव्हा तुम्हाला "next-tick" तातडीची गरज नसते, तेव्हा setImmediate किंवा नियमित promise ला प्राधान्य द्या.
  • setTimeout(fn, 0) आणि setImmediate एकमेकांऐवजी वापरता येतील असे समजू नका. जर क्रम महत्त्वाचा असेल, तर I/O callbacks मधील क्रम तपासा.

मुख्य निष्कर्ष

इव्हेंट लूप हा होस्ट-विशिष्ट शेड्युलर (scheduler) आहे, तो JavaScript चे वैश्विक वैशिष्ट्य नाही. ब्राउझर्स रेंडरिंगला लूपमध्ये गुंफतात; तर Node, I/O ला libuv फेजेसमध्ये (phases) वेगळे करते. प्राधान्य यंत्रणांचा (priority mechanisms) चुकीचा वापर करणे—जसे की ब्राउझरमधील microtasks किंवा Node मधील process.nextTick—त्या सिस्टमच्या भागाला संसाधनांपासून वंचित ठेवू शकते ज्यासाठी प्रत्येक वातावरण (environment) तयार करण्यात आले आहे. तुमचे async पॅटर्न होस्टच्या लूप मॉडेलशी सुसंगत ठेवा, ज्यामुळे तुम्ही फ्रीझ झालेले पेजेस आणि ब्लॉक झालेले सर्व्हर्स या दोन्हीपासून वाचू शकाल.