നിങ്ങളുടെ വെബ് പേജിനെ നിയന്ത്രിക്കുന്ന JavaScript ഇവന്റ് ലൂപ്പും (event loop), ഒരു Node.js സെർവറിനെ നിയന്ത്രിക്കുന്നതും തമ്മിൽ വലിയ വ്യത്യാസങ്ങളുണ്ട്. ഈ വ്യത്യാസം ശ്രദ്ധിച്ചില്ലെങ്കിൽ UI ഫ്രീസ് ആകാനോ I/O തടസ്സപ്പെടാനോ (choke) സാധ്യതയുണ്ട്. രണ്ട് എൻവയോൺമെന്റുകളിലും പ്രവർത്തിക്കുന്ന അസിങ്ക് (async) കോഡ് എഴുതുന്നവർ ഈ രണ്ട് ലൂപ്പുകളും എവിടെയാണ് വ്യത്യസ്തമാകുന്നതെന്ന് അറിഞ്ഞിരിക്കേണ്ടത് അത്യാവശ്യമാണ്.
എന്തുകൊണ്ടാണ് ഈ വ്യത്യാസം പ്രധാനമാകുന്നത്
ഇവന്റ് ലൂപ്പ് എന്നത് ECMAScript സ്പെസിഫിക്കേഷനിൽ നിർവചിക്കപ്പെട്ട ഒന്നല്ല; അത് ഹോസ്റ്റിലാണ് (host) നിലനിൽക്കുന്നത്. ബ്രൗസറുകൾ ഫ്രെയിമുകൾ റെൻഡർ ചെയ്യുമ്പോൾ പേജ് റെസ്പോൺസീവ് ആയി നിലനിർത്താൻ ശ്രമിക്കുന്നു, എന്നാൽ Node.js നിർമ്മിച്ചിരിക്കുന്നത് നോൺ-ബ്ലോക്കിംഗ് I/O അടിസ്ഥാനമാക്കിയാണ്. ഒരു ഹോസ്റ്റിൽ പ്രവർത്തിക്കുന്ന രീതികൾ മറ്റൊരിടത്ത് ഉപയോഗിക്കുന്നത് തിരിച്ചറിയാൻ പ്രയാസമുള്ള ബഗുകൾക്ക് കാരണമായേക്കാം: ഉദാഹരണത്തിന്, നീളമുള്ള പ്രോമിസ് ചെയിനുകൾ (promises) ഒരു ബ്രൗസറിന്റെ റീപെയിന്റിനെ (repaint) തടഞ്ഞേക്കാം, എന്നാൽ നിയന്ത്രണമില്ലാത്ത ഒരു process.nextTick ലൂപ്പ് Node-നെ അതിന്റെ I/O ഘട്ടങ്ങളിലേക്ക് എത്താൻ അനുവദിക്കാതെ തടഞ്ഞേക്കാം.
ബ്രൗസറിലെ ടേൺ-ബേസ്ഡ് ലൂപ്പ് (The browser’s turn-based loop)
ഒരു ബ്രൗസറിൽ, ടാസ്ക് എക്സിക്യൂഷൻ, മൈക്രോടാസ്ക് ഡ്രെയിനിംഗ് (microtask draining), റെൻഡറിംഗ് എന്നിവ ഉൾക്കൊള്ളുന്ന ഒരു സിംഗിൾ സൈക്കിൾ ആണ് ലൂപ്പ് പ്രവർത്തിപ്പിക്കുന്നത്:
- ഒരു മാക്രോടാസ്ക് പ്രവർത്തിപ്പിക്കുക (ഒരു ക്ലിക്ക് ഹാൻഡ്ലർ, ഒരു
setTimeoutമുതലായവ). - എല്ലാ മൈക്രോടാസ്ക്കുകളും (promises,
queueMicrotask) പൂർത്തിയാക്കുക. - ഒരു ഫ്രെയിം വരാനിരിക്കുകയാണെങ്കിൽ, ലക്ഷ്യമിട്ട 60 fps ലഭിക്കുന്നതിനായി പെയിന്റ് ചെയ്യുകയും കോമ്പോസിറ്റ് ചെയ്യുകയും ചെയ്യുക.
- ആവർത്തിക്കുക.
ഈ സൈക്കിളിൽ ഇടപെടാൻ രണ്ട് API-കൾ ഡെവലപ്പർമാർക്ക് അവസരം നൽകുന്നു:
requestAnimationFrame– ബ്രൗസർ പെയിന്റ് ചെയ്യുന്നതിന് തൊട്ടുമുമ്പ് ഇത് വിളിക്കപ്പെടുന്നു. ആനിമേഷൻ ജോലികൾക്കായി ഇത് ഉപയോഗിക്കുന്നതാണ് ഉചിതം, കാരണം നിലവിലുള്ള മൈക്രോടാസ്ക്കുകൾക്ക് ശേഷം എന്നാൽ അടുത്ത ഫ്രെയിമിന് മുമ്പ് ഇതിന്റെ കോൾബാക്ക് പ്രവർത്തിക്കുന്നു.requestIdleCallback– ബ്രൗസറിന് ഉയർന്ന മുൻഗണനയുള്ള ജോലികളൊന്നുമില്ലാത്ത സമയത്ത് ഇത് പ്രവർത്തിക്കുന്നു. അനലിറ്റിക്സ് അല്ലെങ്കിൽ ഡാറ്റ പ്രീ-ലോഡിംഗ് പോലുള്ള കുറഞ്ഞ സ്വാധീനമുള്ള ജോലികൾക്കായി ഇത് ഉപയോഗപ്രദമാണ്.
അപകടം: മൈക്രോടാസ്ക് സ്റ്റാർവേഷൻ (microtask starvation)
ബ്രൗസർ റെൻഡറിംഗിന് മുമ്പ് മൈക്രോടാസ്ക് ക്യൂ പൂർണ്ണമായും കാലിയാക്കുന്നതിനാൽ, നീളമുള്ള പ്രോമിസ് ചെയിനുകൾ UI റെൻഡറിംഗ് നടക്കാതെ തടഞ്ഞേക്കാം. ഇവിടെ കോൾ സ്റ്റാക്ക് ബ്ലോക്ക് ചെയ്യപ്പെടുന്നില്ല; പകരം പേജ് റെൻഡർ ഘട്ടത്തിൽ എത്തുന്നില്ല, ഇത് ഉപയോക്താവിന് പേജ് ഫ്രീസ് ആയതുപോലെ തോന്നിപ്പിക്കുന്നു.
Node-ന്റെ libuv-ഡ്രൈവൻ ലൂപ്പ്
Node.js അതിന്റെ ലൂപ്പിനെ libuv എന്നൊരു C ലൈബ്രറിക്ക് കൈമാറുന്നു. ഇത് ജോലികളെ വ്യത്യസ്ത ഘട്ടങ്ങളായി (phases) തിരിക്കുന്നു, ഓരോന്നിനും അതിന്റേതായ ക്യൂ ഉണ്ട്:
- Timers –
setTimeout,setIntervalഎന്നിവയിൽ നിന്നുള്ള കോൾബാക്കുകൾ. - Pending callbacks – OS തലത്തിൽ ഇതിനകം പൂർത്തിയായ I/O കോൾബാക്കുകൾ.
- Poll – പുതിയ I/O ഇവന്റുകൾ (ഫയൽ റീഡുകൾ, നെറ്റ്വർക്ക് ഡാറ്റ) ശേഖരിക്കുന്നു.
- Check –
setImmediateകോൾബാക്കുകൾ പ്രവർത്തിപ്പിക്കുന്നു. - Close callbacks – ഒരു സോക്കറ്റോ ഹാൻഡിലോ ക്ലോസ് ചെയ്യുമ്പോൾ ഇത് പ്രവർത്തിക്കുന്നു.
ഈ ഘട്ടക്രമത്തിന് പുറത്തായി രണ്ട് ഘടനകളുണ്ട്:
process.nextTick– നിലവിലെ ഓപ്പറേഷൻ അവസാനിച്ച ഉടൻ തന്നെ, മൈക്രോടാസ്ക് ക്യൂവിന് മുമ്പ് ഇത് പ്രവർത്തിക്കുന്നു.
അപകടം: I/O സ്റ്റാർവേഷൻ (I/O starvation)
ഒരു ഫംഗ്ഷൻ ഇടയ്ക്കിടെ നിയന്ത്രണങ്ങൾ വിട്ടുനൽകാതെ (yielding) process.nextTick ഷെഡ്യൂൾ ചെയ്തുകൊണ്ടിരിക്കുകയാണെങ്കിൽ, Node ഒരിക്കലും "next-tick" ഘട്ടത്തിന് അപ്പുറത്തേക്ക് നീങ്ങില്ല. ഇത് നെറ്റ്വർക്ക് റിക്വസ്റ്റുകൾ, ഫയൽ റീഡുകൾ, ടൈമറുകൾ എന്നിവ തടസ്സപ്പെടാനും സെർവർ സൈഡ് ലേറ്റൻസി വർദ്ധിക്കാനും കാരണമാകും.
പ്രായോഗികമായി setImmediate vs. setTimeout
രണ്ടും അടുത്ത ഇറ്ററേഷനായി കോൾബാക്കുകൾ ഷെഡ്യൂൾ ചെയ്യുന്നുണ്ടെങ്കിലും, അവയുടെ ക്രമം അവ എവിടെ നിന്ന് വിളിക്കപ്പെടുന്നു എന്നതിനെ ആശ്രയിച്ചിരിക്കും:
- Top-level code – ഇവയുടെ ക്രമം ഉറപ്പില്ല; പ്രോസസ്സ് എത്ര വേഗത്തിൽ സ്റ്റാർട്ട് ആകുന്നു എന്നതിനെ ആശ്രയിച്ചിരിക്കും ഇത്.
- Inside an I/O callback – ഇവിടെ ക്രമം നിശ്ചിതമാണ്:
setImmediate,setTimeout(fn, 0)-ന് മുമ്പ് പ്രവർത്തിക്കുന്നു. Poll ഘട്ടം അവസാനിച്ചാൽ, സീറോ-ഡിലേ ടൈമൗട്ടിനായി Timers ഘട്ടത്തിലേക്ക് തിരികെ പ്രവേശിക്കുന്നതിന് മുമ്പ് libuv Check ഘട്ടത്തിലേക്ക് (ഇവിടെയാണ്setImmediateഉള്ളത്) നീങ്ങുന്നു.
ഒരു റീഡ് പൂർത്തിയായ ഉടൻ തന്നെ ഒരു റിസോഴ്സ് ക്ലീൻ അപ്പ് ചെയ്യുന്നത് പോലുള്ള കൃത്യമായ ക്രമം ആവശ്യമായ കാര്യങ്ങളിൽ ഈ സൂക്ഷ്മത പ്രധാനമാണ്.
പ്രധാന വ്യത്യാസങ്ങൾ ഒറ്റനോട്ടത്തിൽ
- ലക്ഷ്യം: ബ്രൗസറുകൾ വിഷ്വൽ അപ്ഡേറ്റുകൾക്ക് മുൻഗണന നൽകുന്നു; 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) നിങ്ങൾ കൈകാര്യം ചെയ്യുന്നുണ്ടെങ്കിൽ, താഴെ പറയുന്ന കാര്യങ്ങൾ പരിശോധിക്കുക:
- ഇവന്റ് ലൂപ്പിന് വിട്ടുനൽകാതെ (yielding) ഒരുപാട് പ്രോമിസുകൾ ചെയിൻ ചെയ്യുന്നുണ്ടോ എന്ന് നോക്കുക. റെൻഡറർക്ക് അവസരം നൽകുന്നതിനായി
await new Promise(r => setTimeout(r, 0))ഉപയോഗിക്കുകയോ ബ്രൗസറിൽrequestIdleCallbackഉപയോഗിക്കുകയോ ചെയ്യുക. - മാറ്റിവെക്കാവുന്ന ജോലികൾക്കായി
process.nextTickഉപയോഗിക്കുന്നുണ്ടോ എന്ന് പരിശോധിക്കുക. അടിയന്തരമായി ചെയ്യേണ്ട ജോലികൾ അല്ലാത്തപ്പോൾsetImmediateഅല്ലെങ്കിൽ ഒരു സാധാരണ പ്രോമിസ് ഉപയോഗിക്കുക. setTimeout(fn, 0),setImmediateഎന്നിവ ഒന്നുതന്നെയാണെന്ന് കരുതരുത്. ക്രമം പ്രധാനമാണെങ്കിൽ I/O കോൾബാക്കുകൾക്കുള്ളിലെ ക്രമം ടെസ്റ്റ് ചെയ്യുക.
പ്രധാന കാര്യങ്ങൾ
ഇവന്റ് ലൂപ്പ് (event loop) എന്നത് ഒരു ഹോസ്റ്റ്-സ്പെസിഫിക് ഷെഡ്യൂളറാണ്, അല്ലാതെ ഒരു സാർവത്രിക JavaScript ഫീച്ചറല്ല. ബ്രൗസറുകൾ റെൻഡറിംഗിനെ ലൂപ്പിന്റെ ഭാഗമാക്കുമ്പോൾ, Node libuv ഘട്ടങ്ങളിലൂടെ I/O പ്രവർത്തനങ്ങളെ വേർതിരിക്കുന്നു. മുൻഗണനാ സംവിധാനങ്ങൾ (priority mechanisms)—ബ്രൗസറിലെ microtasks, Node-ലെ process.nextTick എന്നിവ—തെറ്റായി ഉപയോഗിക്കുന്നത് ഓരോ എൻവയോൺമെന്റും സേവിക്കാൻ നിർമ്മിക്കപ്പെട്ടിട്ടുള്ള സിസ്റ്റത്തിന്റെ ഭാഗങ്ങളെ തടസ്സപ്പെടുത്തിയേക്കാം. നിങ്ങളുടെ async പാറ്റേണുകൾ ഹോസ്റ്റിന്റെ ലൂപ്പ് മോഡലിന് അനുസൃതമാക്കുകയാണെങ്കിൽ, ഫ്രീസ് ആയ പേജുകളും ബ്ലോക്ക് ആയ സെർവറുകളും ഒരേപോലെ ഒഴിവാക്കാൻ സാധിക്കും.
