ನಿಮ್ಮ ವೆಬ್ ಪುಟವನ್ನು ನಿಯಂತ್ರಿಸುವ JavaScript event loop ಮತ್ತು Node.js ಸರ್ವರ್ ಅನ್ನು ನಿಯಂತ್ರಿಸುವ event loop ನಡುವೆ ಬಹಳ ವ್ಯತ್ಯಾಸವಿದೆ. ನೀವು ಎಚ್ಚರಿಕೆಯಿಂದ ಇಲ್ಲದಿದ್ದರೆ, ಈ ವ್ಯತ್ಯಾಸವು UI ಅನ್ನು ಸ್ಥಗಿತಗೊಳಿಸಬಹುದು (freeze) ಅಥವಾ I/O ಅನ್ನು ಕುಂಠಿತಗೊಳಿಸಬಹುದು. ಎರಡೂ ಪರಿಸರಗಳಲ್ಲಿ (environments) ಚಲಿಸುವ async ಕೋಡ್ ಬರೆಯುವವರಿಗೆ ಇವೆರಡರ ನಡುವಿನ ವ್ಯತ್ಯಾಸವನ್ನು ತಿಳಿಯುವುದು ಅತ್ಯಗತ್ಯ.

ಈ ವ್ಯತ್ಯಾಸ ಏಕೆ ಮುಖ್ಯ?

Event loop ಅನ್ನು ECMAScript ಸ್ಪೆಕ್ (spec) ನಿಂದ ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ; ಇದು host ನಲ್ಲಿ ಇರುತ್ತದೆ. ಬ್ರೌಸರ್‌ಗಳು ಫ್ರೇಮ್‌ಗಳನ್ನು ರೆಂಡರ್ ಮಾಡುವಾಗ ಪುಟವು ಪ್ರತಿಕ್ರಿಯಾತ್ಮಕವಾಗಿರುವಂತೆ (responsive) ನೋಡಿಕೊಳ್ಳಬೇಕು, ಆದರೆ Node.js ಅನ್ನು non-blocking I/O ಸುತ್ತ ನಿರ್ಮಿಸಲಾಗಿದೆ. ಒಂದು host ನಲ್ಲಿ ಕೆಲಸ ಮಾಡುವ ಪ್ಯಾಟರ್ನ್‌ಗಳನ್ನು ಇನ್ನೊಂದರೊಂದಿಗೆ ಬೆರೆಸುವುದರಿಂದ ಪತ್ತೆಹಚ್ಚಲು ಕಷ್ಟವಾದ ಬಗ್‌ಗಳು (bugs) ಉಂಟಾಗಬಹುದು: ಒಂದು ಉದ್ದನೆಯ promises ಸರಪಳಿಯು ಬ್ರೌಸರ್‌ನ repaint ಅನ್ನು ತಡೆಹಿಡಿಯಬಹುದು, ಆದರೆ ಅನ್‌ಚೆಕ್ ಮಾಡದ process.nextTick loop Node ಅನ್ನು ಅದರ I/O ಹಂತಗಳಿಗೆ ತಲುಪದಂತೆ ತಡೆಯಬಹುದು.

ಬ್ರೌಸರ್‌ನ ಟರ್ನ್-ಬೇಸ್ಡ್ ಲೂಪ್ (turn-based loop)

ಬ್ರೌಸರ್‌ನಲ್ಲಿ, ಲೂಪ್ ಒಂದು ಏಕೈಕ ಸೈಕಲ್ ಅನ್ನು ರನ್ ಮಾಡುತ್ತದೆ, ಇದು task execution, microtask draining ಮತ್ತು rendering ಅನ್ನು ಅಂತರಾಳದಲ್ಲಿ (interleave) ನಡೆಸುತ್ತದೆ:

  1. ಒಂದು macrotask ಅನ್ನು ರನ್ ಮಾಡಿ (ಉದಾಹರಣೆಗೆ click handler, setTimeout ಇತ್ಯಾದಿ).
  2. ಎಲ್ಲಾ microtasks ಅನ್ನು ಖಾಲಿ ಮಾಡಿ (promises, queueMicrotask).
  3. ಒಂದು ಫ್ರೇಮ್ ಬರಬೇಕಿದ್ದರೆ, 60 fps ಗುರಿಯನ್ನು ತಲುಪಲು paint ಮತ್ತು composite ಮಾಡಿ.
  4. ಪುನರಾವರ್ತಿಸಿ.

ಎರಡು APIಗಳು ಈ ಸೈಕಲ್‌ಗೆ ಡೆವಲಪರ್‌ಗಳಿಗೆ ಸ್ಪಷ್ಟವಾದ ಹೂಕ್‌ಗಳನ್ನು (hooks) ನೀಡುತ್ತವೆ:

  • requestAnimationFrame – ಬ್ರೌಸರ್ ಪೇಂಟ್ ಮಾಡುವ ಮೊದಲು ಇದನ್ನು ಕರೆಯಲಾಗುತ್ತದೆ. ಅನಿಮೇಷನ್ ಕೆಲಸಗಳಿಗೆ ಇದು ಸರಿಯಾದ ಸ್ಥಳವಾಗಿದೆ ಏಕೆಂದರೆ ಈ callback ಪ್ರಸ್ತುತ microtasks ನಂತರ ಆದರೆ ಮುಂದಿನ ಫ್ರೇಮ್‌ಗಿಂತ ಮೊದಲು ರನ್ ಆಗುತ್ತದೆ.
  • requestIdleCallback – ಬ್ರೌಸರ್‌ನಲ್ಲಿ ಯಾವುದೇ ಹೆಚ್ಚಿನ ಆದ್ಯತೆಯ ಕೆಲಸವಿಲ್ಲದಿದ್ದಾಗ ಇದನ್ನು ಕರೆಯಲಾಗುತ್ತದೆ. ಅನಾಲಿಟಿಕ್ಸ್ ಅಥವಾ ಡೇಟಾ ಪ್ರೀ-ಲೋಡಿಂಗ್‌ನಂತಹ ಕಡಿಮೆ ಪ್ರಭಾವ ಬೀರುವ ಕಾರ್ಯಗಳಿಗೆ ಇದು ಉಪಯುಕ್ತವಾಗಿದೆ.

ಅಪಾಯ: microtask starvation

ಬ್ರೌಸರ್ ರೆಂಡರ್ ಮಾಡುವ ಮೊದಲು microtask queue ಅನ್ನು ಖಾಲಿ ಮಾಡುವುದರಿಂದ, ಉದ್ದನೆಯ promises ಸರಪಳಿಯು UI ಪೇಂಟ್ ಆಗದಂತೆ ತಡೆಯಬಹುದು. ಇಲ್ಲಿ call stack ಬ್ಲಾಕ್ ಆಗುವುದಿಲ್ಲ; ಬದಲಾಗಿ ಪುಟವು ರೆಂಡರ್ ಹಂತವನ್ನು ತಲುಪುವುದಿಲ್ಲ, ಇದು ಬಳಕೆದಾರರಿಗೆ ಫ್ರೀಜ್ ಆದಂತೆ ಕಾಣಿಸುತ್ತದೆ.

Node ನ libuv-ಚಾಲಿತ ಲೂಪ್

Node.js ತನ್ನ ಲೂಪ್ ಅನ್ನು libuv ಗೆ ವಹಿಸುತ್ತದೆ. ಇದು ಒಂದು C ಲೈಬ್ರರಿಯಾಗಿದ್ದು, ಕೆಲಸವನ್ನು ವಿಭಿನ್ನ ಹಂತಗಳಾಗಿ ವಿಂಗಡಿಸುತ್ತದೆ ಮತ್ತು ಪ್ರತಿಯೊಂದು ಹಂತಕ್ಕೂ ತನ್ನದೇ ಆದ ಕ್ಯೂ (queue) ಇರುತ್ತದೆ:

  1. TimerssetTimeout ಮತ್ತು setInterval ನಿಂದ ಬರುವ callbacks.
  2. Pending callbacks – OS ಮಟ್ಟದಲ್ಲಿ ಈಗಾಗಲೇ ಪೂರ್ಣಗೊಂಡಿರುವ ವಿಳಂಬಿತ I/O callbacks.
  3. Poll – ಹೊಸ I/O ಇವೆಂಟ್‌ಗಳನ್ನು (file reads, network data) ಪಡೆಯುತ್ತದೆ.
  4. ChecksetImmediate callbacks ಅನ್ನು ರನ್ ಮಾಡುತ್ತದೆ.
  5. Close callbacks – ಸಾಕೆಟ್ ಅಥವಾ ಹ್ಯಾಂಡಲ್ ಮುಚ್ಚಿದಾಗ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತದೆ.

ಎರಡು ರಚನೆಗಳು (constructs) ಈ ಹಂತದ ಕ್ರಮದ ಹೊರಗೆ ಇರುತ್ತವೆ:

  • process.nextTick – ಪ್ರಸ್ತುತ ಆಪರೇಷನ್ ಮುಗಿದ ತಕ್ಷಣ, microtask queue ಗಿಂತ ಮೊದಲು ರನ್ ಆಗುತ್ತದೆ.

ಅಪಾಯ: I/O starvation

ಒಂದು ಫಂಕ್ಷನ್ ಯೆಲ್ಡಿಂಗ್ (yielding) ಮಾಡದೆ ಪದೇ ಪದೇ process.nextTick ಅನ್ನು ಶೆಡ್ಯೂಲ್ ಮಾಡಿದರೆ, Node ಎಂದಿಗೂ "next-tick" ಹಂತದ ನಂತರಕ್ಕೆ ಸಾಗುವುದಿಲ್ಲ. ನೆಟ್‌ವರ್ಕ್ ರಿಕ್ವೆಸ್ಟ್‌ಗಳು, ಫೈಲ್ ರೀಡ್‌ಗಳು ಮತ್ತು ಟೈಮರ್‌ಗಳು ನಿಷ್ಕ್ರಿಯವಾಗಿರುತ್ತವೆ, ಇದು ಸರ್ವರ್-ಸೈಡ್ ವಿಳಂಬ (latency spikes) ಅಥವಾ ಸಂಪೂರ್ಣವಾಗಿ ಹ್ಯಾಂಗ್ ಆಗಲು ಕಾರಣವಾಗಬಹುದು.

ಪ್ರಾಯೋಗಿಕವಾಗಿ setImmediate vs. setTimeout

ಎರಡೂ ಮುಂದಿನ ಇಟರೇಶನ್‌ಗಾಗಿ callbacks ಅನ್ನು ಶೆಡ್ಯೂಲ್ ಮಾಡುತ್ತವೆ, ಆದರೆ ಅವುಗಳನ್ನು ಎಲ್ಲಿ ಕರೆಯಲಾಗುತ್ತದೆ ಎಂಬುದರ ಮೇಲೆ ಅವುಗಳ ಕ್ರಮ ನಿರ್ಧರಿಸಲ್ಪಡುತ್ತದೆ:

  • Top-level code – ಕ್ರಮವು ಖಚಿತವಾಗಿರುವುದಿಲ್ಲ; ಇದು ಪ್ರೊಸೆಸ್ ಎಷ್ಟು ವೇಗವಾಗಿ ಪ್ರಾರಂಭವಾಗುತ್ತದೆ ಎಂಬುದರ ಮೇಲೆ ಅವಲಂಬಿತವಾಗಿರುತ್ತದೆ.
  • Inside an I/O callback – ಕ್ರಮವು ನಿರ್ಣಾಯಕವಾಗಿರುತ್ತದೆ (deterministic): setImmediate ಎಂಬುದು setTimeout(fn, 0) ಗಿಂತ ಮೊದಲು ರನ್ ಆಗುತ್ತದೆ. Poll ಹಂತವು ಮುಗಿದ ನಂತರ, libuv ಶೂನ್ಯ-ವಿಳಂಬದ (zero-delay) ಟೈಮೌಟ್‌ಗಾಗಿ Timers ಹಂತಕ್ಕೆ ಮರುಪ್ರವೇಶಿಸುವ ಮೊದಲು Check ಹಂತಕ್ಕೆ (ಇಲ್ಲಿ setImmediate ಇರುತ್ತದೆ) ಚಲಿಸುತ್ತದೆ.

ರೀಡ್ (read) ಪೂರ್ಣಗೊಂಡ ತಕ್ಷಣ ಸಂಪನ್ಮೂಲವನ್ನು (resource) ಕ್ಲೀನ್ ಅಪ್ ಮಾಡುವಂತಹ ನಿಖರವಾದ ಅನುಕ್ರಮದ ಅಗತ್ಯವಿದ್ದಾಗ ಈ ಸೂಕ್ಷ್ಮತೆ ಮುಖ್ಯವಾಗುತ್ತದೆ.

ಪ್ರಮುಖ ವ್ಯತ್ಯಾಸಗಳು ಒಮ್ಮೆ ನೋಡಿ

  • Goal (ಗುರಿ): ಬ್ರೌಸರ್‌ಗಳು ದೃಶ್ಯ ಅಪ್‌ಡೇಟ್‌ಗಳಿಗೆ ಆದ್ಯತೆ ನೀಡುತ್ತವೆ; Node I/O ಸಿದ್ಧತೆಗೆ ಆದ್ಯತೆ ನೀಡುತ್ತದೆ.
  • Rendering hook: requestAnimationFrame (ಬ್ರೌಸರ್ ಮಾತ್ರ).
  • Phase-specific hook: setImmediate (Node ಮಾತ್ರ, Check ಹಂತದಲ್ಲಿ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತದೆ).
  • High-priority queue: process.nextTick (Node ಮಾತ್ರ, microtasks ಗಿಂತ ಮೊದಲು ರನ್ ಆಗುತ್ತದೆ).
  • Starvation risk: ಬ್ರೌಸರ್‌ಗಳಲ್ಲಿ ಉದ್ದನೆಯ promise ಸರಪಳಿಗಳು; Node ನಲ್ಲಿ ಅನ್‌ಬೌಂಡೆಡ್ (unbounded) process.nextTick.

ಮುಂದೆ ಏನು ಗಮನಿಸಬೇಕು

ನೀವು ಎರಡೂ ಪರಿಸರಗಳಲ್ಲಿ ಚಲಿಸುವ ಕೋಡ್ ಬೇಸ್ ಅನ್ನು ನಿರ್ವಹಿಸುತ್ತಿದ್ದರೆ (ಉದಾಹರಣೆಗೆ, isomorphic libraries), ಈ ಕೆಳಗಿನವುಗಳನ್ನು ಪರಿಶೀಲಿಸಿ:

  • Event loop ಗೆ ಯೆಲ್ಡ್ ಮಾಡದೆ ಅನೇಕ promises ಅನ್ನು ಚೇನ್ ಮಾಡಬೇಡಿ. ರೆಂಡರರ್‌ಗೆ ಅವಕಾಶ ನೀಡಲು await new Promise(r => setTimeout(r, 0)) ಅನ್ನು ಸೇರಿಸಿ ಅಥವಾ ಬ್ರೌಸರ್‌ನಲ್ಲಿ requestIdleCallback ಬಳಸಿ.
  • ವಿಳಂಬ ಮಾಡಬಹುದಾದ ಕೆಲಸಗಳಿಗಾಗಿ process.nextTick ಅನ್ನು ಬಳಸಬೇಡಿ. ನಿಮಗೆ "next-tick" ತುರ್ತು ಅಗತ್ಯವಿಲ್ಲದಿದ್ದಾಗ setImmediate ಅಥವಾ ಸಾಮಾನ್ಯ promise ಅನ್ನು ಆದ್ಯತೆಯಾಗಿ ಬಳಸಿ.
  • setTimeout(fn, 0) ಮತ್ತು setImmediate ಒಂದೇ ಎಂಬ ತಪ್ಪು ಕಲ್ಪನೆ ಇಟ್ಟುಕೊಳ್ಳಬೇಡಿ. ಕ್ರಮವು ಮುಖ್ಯವಾಗಿದ್ದರೆ, I/O callbacks ಒಳಗಿನ ಆರ್ಡರ್ ಅನ್ನು ಪರೀಕ್ಷಿಸಿ.

ಸಾರಾಂಶ

event loop ಎಂಬುದು ಹೋಸ್ಟ್-ನಿರ್ದಿಷ್ಟ ಶೆಡ್ಯೂಲರ್ ಆಗಿದೆಯೇ ಹೊರತು, ಇದು ಸಾರ್ವತ್ರಿಕ JavaScript ವೈಶಿಷ್ಟ್ಯವಲ್ಲ. ಬ್ರೌಸರ್‌ಗಳು ರೆಂಡರಿಂಗ್ ಅನ್ನು ಲೂಪ್‌ನಲ್ಲಿ ಅಳವಡಿಸುತ್ತವೆ; Node ಎಂಬುದು I/O ಅನ್ನು libuv ಹಂತಗಳಿಗೆ ಪ್ರತ್ಯೇಕಿಸುತ್ತದೆ. ಆದ್ಯತೆಯ ಕಾರ್ಯವಿಧಾನಗಳನ್ನು—ಬ್ರೌಸರ್‌ನಲ್ಲಿ microtasks ಮತ್ತು Node ನಲ್ಲಿ process.nextTick—ತಪ್ಪಾಗಿ ಬಳಸುವುದರಿಂದ, ಪ್ರತಿಯೊಂದು ಎನ್ವಿರಾನ್ಮೆಂಟ್ ಯಾವ ಉದ್ದೇಶಕ್ಕಾಗಿ ನಿರ್ಮಿತವಾಗಿದೆಯೋ, ಆ ಸಿಸ್ಟಮ್‌ನ ಭಾಗಕ್ಕೆ ಸಂಪನ್ಮೂಲಗಳ ಕೊರತೆ ಉಂಟಾಗಬಹುದು. ನಿಮ್ಮ async ಪ್ಯಾಟರ್ನ್‌ಗಳನ್ನು ಹೋಸ್ಟ್‌ನ ಲೂಪ್ ಮಾಡೆಲ್‌ಗೆ ಅನುಗುಣವಾಗಿ ಹೊಂದಿಸಿಕೊಳ್ಳಿ, ಆಗ ನೀವು ಫ್ರೀಜ್ ಆದ ಪೇಜ್‌ಗಳು ಮತ್ತು ಬ್ಲಾಕ್ ಆದ ಸರ್ವರ್‌ಗಳಂತಹ ಸಮಸ್ಯೆಗಳನ್ನು ತಪ್ಪಿಸಬಹುದು.