നിങ്ങൾ എപ്പോഴെങ്കിലും ഒരു tooltip മുകളിൽ ഇടത് കോണിൽ നിന്ന് അതിന്റെ ശരിയായ സ്ഥാനത്തേക്ക് പെട്ടെന്ന് മാറുന്നത് കണ്ടിട്ടുണ്ടോ? അല്ലെങ്കിൽ ഒരു modal അതിന്റെ ശരിയായ വലുപ്പത്തിലേക്ക് മാറുന്നതിന് മുമ്പ് തെറ്റായ വലുപ്പത്തിൽ മിന്നിമറയുന്നത് ശ്രദ്ധിച്ചിട്ടുണ്ടോ? ആ നിമിഷനേരത്തെ തകരാറിനെയാണ് layout flicker എന്ന് പറയുന്നത്. React DOM വായിക്കുകയും, ഒരു തിരുത്തൽ കണക്കാക്കുകയും, state അപ്‌ഡേറ്റ് ചെയ്യുകയും ചെയ്യുമ്പോൾ, ബ്രൗസർ ഇതിനകം തന്നെ സ്ക്രീനിൽ പിക്സലുകൾ കാണിക്കാൻ തുടങ്ങുന്നതാണ് ഇതിന് കാരണം. ഇതിനുള്ള സാധാരണ പരിഹാരം useEffect-ന് പകരം useLayoutEffect ഉപയോഗിക്കുക എന്നതാണ്. ഈ മാറ്റം ഫലപ്രദമാണ്, പക്ഷേ ബ്രൗസർ പൈപ്പ്‌ലൈനിനുള്ളിൽ ഓരോ ഹുക്കും (hook) കൃത്യമായി എപ്പോഴാണ് പ്രവർത്തിക്കുന്നത് എന്ന് നിങ്ങൾ മനസ്സിലാക്കിയാൽ മാത്രമേ ഇത് ശരിയായി പ്രവർത്തിക്കൂ.

ബ്രൗസർ പൈപ്പ്‌ലൈൻ: Render, Commit, Paint

React ഒരു കോമ്പൊണന്റിനെ മൂന്ന് വ്യത്യസ്ത ഘട്ടങ്ങളിലായാണ് അപ്‌ഡേറ്റ് ചെയ്യുന്നത്. render ഘട്ടത്തിൽ, React Virtual DOM നിർമ്മിക്കുകയോ അല്ലെങ്കിൽ പുതുക്കുകയോ ചെയ്യുകയും അതിലെ വ്യത്യാസങ്ങൾ (diff) കണക്കാക്കുകയും ചെയ്യുന്നു. ഇവിടെ പിക്സലുകളിൽ മാറ്റം വരുന്നില്ല; ഇത് മെമ്മറിയിൽ നടക്കുന്ന കണക്കുകൂട്ടലുകൾ മാത്രമാണ്. അടുത്തത് commit ഘട്ടമാണ്, ഇവിടെ React ആ മാറ്റങ്ങൾ യഥാർത്ഥ DOM നോഡുകളിൽ പ്രയോഗിക്കുന്നു. സ്റ്റൈലുകൾ മാറുന്നു, നോഡുകൾ ചേർക്കപ്പെടുകയോ നീക്കം ചെയ്യപ്പെടുകയോ ചെയ്യുന്നു, ടെക്സ്റ്റുകളിൽ മാറ്റം വരുന്നു.

അതിനുശേഷം ബ്രൗസർ നിയന്ത്രണം ഏറ്റെടുക്കുന്നു. paint ഘട്ടത്തിൽ, ബ്രൗസറിലെ റെൻഡറിംഗ് എഞ്ചിൻ ലേഔട്ട് ജിയോമെട്രി കണക്കാക്കുകയും സ്ക്രീനിൽ പിക്സലുകൾ വരയ്ക്കുകയും ചെയ്യുന്നു. ഈ ക്രമം മാറ്റമില്ലാത്തതാണ്. പെയിന്റ് ചെയ്യുന്നതിന് മുമ്പ് ബ്രൗസർ ലേഔട്ട് പൂർത്തിയാക്കണം, കൂടാതെ ഉപയോക്താവ് പുതിയതായി എന്തെങ്കിലും കാണുന്നതിന് മുമ്പ് പെയിന്റിംഗ് പൂർത്തിയാകുകയും വേണം. commit-നും paint-നും ഇടയിലുള്ള വ്യത്യാസം മില്ലിസെക്കൻഡുകളിൽ ആണെങ്കിലും, അത് യഥാർത്ഥമാണ്. useEffect-ഉം useLayoutEffect-ഉം തമ്മിലുള്ള വ്യത്യാസം പ്രകടമാകുന്നത് ഈ സമയത്താണ്.

എന്തുകൊണ്ടാണ് useEffect ഫ്ലിക്കറിന് (flicker) കാരണമാകുന്നത്?

ബ്രൗസർ സ്ക്രീൻ പെയിന്റ് ചെയ്തതിന് ശേഷം പ്രവർത്തിപ്പിക്കാനായി ഷെഡ്യൂൾ ചെയ്തിരിക്കുന്നതിനാൽ useEffect അസിൻക്രണസ് (asynchronously) ആയിട്ടാണ് പ്രവർത്തിക്കുന്നത്. DOM അപ്‌ഡേറ്റ് ചെയ്യപ്പെടുന്നു, പിക്സലുകൾ വരയ്ക്കപ്പെടുന്നു, അതിനുശേഷം നിങ്ങളുടെ effect പ്രവർത്തിപ്പിക്കാൻ React വീണ്ടും ഇടപെടുന്നു.

ഒരു ബട്ടണിന് താഴെ നിങ്ങൾ ഒരു dropdown മെനു റെൻഡർ ചെയ്യുന്നു എന്ന് കരുതുക. useEffect-നുള്ളിൽ, നിങ്ങൾ buttonRef.current.getBoundingClientRect() വിളിക്കുകയും ശരിയായ top, left കോർഡിനേറ്റുകൾ കണക്കാക്കി അവ state-ൽ സൂക്ഷിക്കുകയും ചെയ്യുന്നു. useEffect പെയിന്റിന് ശേഷം പ്രവർത്തിക്കുന്നതിനാൽ, ബ്രൗസർ ഇതിനകം തന്നെ dropdown അതിന്റെ ഡിഫോൾട്ട് സ്ഥാനത്ത് (ഒരുപക്ഷേ top: 0, left: 0 എന്ന നിലയിൽ) വരച്ചു കഴിഞ്ഞിട്ടുണ്ടാകും. ആ പെയിന്റിംഗിന് ശേഷം മാത്രമേ നിങ്ങളുടെ effect state അപ്‌ഡേറ്റ് ചെയ്യൂ. തുടർന്ന് React തിരുത്തിയ കോർഡിനേറ്റുകൾ commit ചെയ്യുന്നു, ബ്രൗസർ വീണ്ടും പെയിന്റ് ചെയ്യുന്നു. ഉപയോക്താവ് രണ്ട് ഫ്രെയിമുകൾ കാണുന്നു: ആദ്യം തെറ്റായ സ്ഥാനവും, പിന്നീട് ശരിയായ സ്ഥാനവും. ഈ പെട്ടെന്നുള്ള മാറ്റമാണ് (visual snap) എല്ലാവരും ഒഴിവാക്കാൻ ശ്രമിക്കുന്ന ആ ഫ്ലിക്കർ.

ഡാറ്റ ഫെച്ചിംഗ് (data fetching), API കോളുകൾ, അനലിറ്റിക്സ് ട്രാക്കിംഗ്, അല്ലെങ്കിൽ ഇവന്റ് ലിസണറുകൾ സെറ്റ് ചെയ്യുക എന്നിവയ്ക്ക് ഈ താമസം പ്രശ്നമല്ല. പെയിന്റിംഗിന് ഏതാനും മില്ലിസെക്കൻഡുകൾക്ക് ശേഷം ഒരു അനലിറ്റിക്സ് ബീക്കൺ പ്രവർത്തിച്ചാലും ഉപയോക്താവിന് അത് പ്രശ്നമാകില്ല. വാസ്തവത്തിൽ, കാഴ്ചയിൽ മാറ്റം വരുത്താത്ത ജോലികൾ പെയിന്റിംഗിന് ശേഷം മാറ്റിവയ്ക്കുന്നത് ആദ്യത്തെ റെൻഡറിംഗ് വേഗത്തിലാക്കാൻ സഹായിക്കും. എന്നാൽ ലേഔട്ടുമായി ബന്ധപ്പെട്ട തിരുത്തലുകൾക്ക് useEffect വളരെ വൈകിപ്പോകുന്നു.

useLayoutEffect എങ്ങനെയാണ് പെയിന്റിംഗിനെ തടയുന്നത് (Blocks the Paint)?

React DOM-ൽ മാറ്റം വരുത്തിയ ഉടൻ തന്നെ, എന്നാൽ ബ്രൗസർക്ക് ലേഔട്ട് കണക്കാക്കാനോ പിക്സലുകൾ പെയിന്റ് ചെയ്യാനോ അവസരം ലഭിക്കുന്നതിന് മുമ്പ് തന്നെ useLayoutEffect സിൻക്രണസ് (synchronously) ആയി പ്രവർത്തിക്കുന്നു. ഇത് പെയിന്റ് പൈപ്പ്‌ലൈനിനെ പൂർണ്ണമായും തടയുന്നു.

നിങ്ങൾ ഇതേ dropdown അളവ് useLayoutEffect-നുള്ളിൽ ചെയ്യുകയാണെങ്കിൽ, അതിന്റെ ക്രമം മാറുന്നു. React ആദ്യത്തെ DOM അപ്‌ഡേറ്റ് commit ചെയ്യുന്നു, നിങ്ങളുടെ ലേഔട്ട് effect പ്രവർത്തിപ്പിക്കുന്നു, തുടർന്ന് നിങ്ങളുടെ state അപ്‌ഡേറ്റ് ഒരു സിൻക്രണസ് റീ-റെൻഡറിംഗിന് കാരണമാകുന്നു. React തിരുത്തിയ കോർഡിനേറ്റുകൾ commit ചെയ്യുന്നു, അതിനുശേഷം മാത്രമേ ബ്രൗസർ പെയിന്റ് ചെയ്യൂ. ഉപയോക്താവ് കാണുന്നത് ശരിയായ രീതിയിലുള്ള ഒരു ഫ്രെയിം മാത്രമാണ്.

ഈ ബ്ലോക്കിംഗ് സ്വഭാവം ഒരു ഗുണവും അതേസമയം ഒരു അപകടസാധ്യതയുമാണ്. useLayoutEffect അതിന്റെ ജോലി പൂർത്തിയാക്കുന്നത് വരെ ബ്രൗസറിനെ പെയിന്റ് ചെയ്യാൻ അനുവദിക്കാത്തതിനാൽ, ഇതിനുള്ളിലെ കനത്ത കണക്കുകൂട്ടലുകൾ (heavy computation) UI-യെ മരവിപ്പിക്കും (freeze). ഏതാനും മില്ലിസെക്കൻഡുകൾ പെയിന്റിംഗ് തടസ്സപ്പെട്ടാൽ പോലും ഉപയോക്താവിന് അത് ഒരു തടസ്സം (jank) പോലെ അനുഭവപ്പെടും. അതുകൊണ്ടാണ്, ആദ്യം useEffect ഉപയോഗിക്കണമെന്നും, ഒഴിവാക്കാൻ കഴിയാത്ത തരത്തിലുള്ള ഫ്ലിക്കർ നേരിട്ട് കാണുമ്പോൾ മാത്രം useLayoutEffect-ലേക്ക് മാറണമെന്നും React ഡോക്യുമെന്റേഷൻ വ്യക്തമായി പറയുന്നത്.

ഓരോ ഹുക്കും എപ്പോൾ ഉപയോഗിക്കണം?

നിങ്ങളുടെ മിക്ക ലോജിക്കുകളും useEffect-ൽ ഉൾപ്പെടുത്തുന്നതാണ് നല്ലത്. ഇത് താഴെ പറയുന്നവയ്ക്കായി ഉപയോഗിക്കാം:

  • ഒരു API-ൽ നിന്ന് ഡാറ്റ ഫെച്ച് ചെയ്യാൻ
  • സബ്‌സ്‌ക്രിപ്ഷനുകളോ ഇവന്റ് ലിസണറുകളോ സെറ്റ് ചെയ്യാൻ
  • അനലിറ്റിക്സ് ഇവന്റുകൾ അയക്കാൻ
  • ലേഔട്ട് ഉടൻ വായിക്കുകയോ മാറ്റം വരുത്തുകയോ ചെയ്യാത്ത ഏതൊരു സൈഡ് ഇഫക്റ്റിനും (side effect)

ഉപയോക്താവ് ഫ്രെയിം കാണുന്നതിന് മുമ്പ് തന്നെ DOM വായിക്കുകയും തിരികെ എഴുതുകയും ചെയ്യേണ്ട കാര്യങ്ങൾക്കായി useLayoutEffect മാറ്റിവെക്കുക:

  • എലമെന്റിന്റെ വീതി (width), ഉയരം (height), അല്ലെങ്കിൽ സ്ക്രോൾ പൊസിഷൻ എന്നിവ അളക്കാൻ
  • tooltip, popover, അല്ലെങ്കിൽ context menus എന്നിവയുടെ കോർഡിനേറ്റുകൾ കണക്കാക്കാൻ
  • വിഷ്വൽ പൊസിഷൻ റെൻഡർ ചെയ്ത ജിയോമെട്രിയെ ആശ്രയിച്ചിരിക്കുമ്പോൾ ലേഔട്ട് മാറുന്നത് (layout shifts) തടയാൻ

ഏതാണ് തിരഞ്ഞെടുക്കേണ്ടതെന്ന് സംശയമുണ്ടെങ്കിൽ, useEffect ഉപയോഗിക്കുക. വിഷ്വൽ ഇൻസ്റ്റബിലിറ്റി (visual instability) ശ്രദ്ധയിൽപ്പെട്ടാൽ മാത്രം useLayoutEffect-ലേക്ക് മാറുക. ഈ ഒരു നിയമം പാലിച്ചാൽ തന്നെ ഭൂരിഭാഗം React ആപ്ലിക്കേഷനുകളും സുഗമമായി പ്രവർത്തിക്കും.

സെർവർ സൈഡ് റെൻഡറിംഗിലെ (Server-Side Rendering) പ്രശ്നങ്ങൾ

നിങ്ങൾ Next.js, Remix അല്ലെങ്കിൽ സെർവറിൽ React റെൻഡർ ചെയ്യുന്ന ഏതെങ്കിലും ഫ്രെയിംവർക്ക് ഉപയോഗിക്കുന്നുണ്ടെങ്കിൽ, useLayoutEffect-ൽ ഒരു വാർണിംഗ് നേരിടേണ്ടി വരും. സെർവറിൽ DOM ഇല്ലാത്തതിനാൽ, ഈ ഹുക്കിന് അളക്കാൻ (measure) ഒന്നുമില്ല. ഒരു ബ്രൗസർ എൻവയോൺമെന്റ് പ്രതീക്ഷിച്ചിരുന്നെങ്കിലും അത് ലഭിച്ചില്ല എന്ന് React നിങ്ങളെ മുന്നറിയിപ്പ് നൽകുന്നു. ഹൈഡ്രേഷൻ (hydration) സമയത്ത്, സെർവർ റെൻഡർ ചെയ്ത മാർക്ക്അപ്പും ക്ലയന്റിന്റെ ആദ്യത്തെ റെൻഡറും തമ്മിൽ വ്യത്യാസമുണ്ടാകാൻ സാധ്യതയുള്ളതിനാൽ ഈ പൊരുത്തക്കേട് ചെറിയ ബഗുകൾക്ക് കാരണമായേക്കാം.

ഇതിനുള്ള സാധാരണ പരിഹാരം, എൻവയോൺമെന്റിനെ അടിസ്ഥാനമാക്കി ശരിയായ ഇഫക്റ്റ് തിരഞ്ഞെടുക്കുന്ന ഒരു isomorphic hook ആണ്:

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

DOM നോഡുകൾ അളക്കേണ്ടതുണ്ട് എന്നാൽ സെർവർ റെൻഡറിംഗിനിടെ പ്രവർത്തിക്കാൻ സാധ്യതയുള്ള ഏതൊരു കമ്പോണന്റിലും ഈ wrapper ഉപയോഗിക്കുക. ഇത് വാർണിംഗ് ഒഴിവാക്കുകയും നിങ്ങളുടെ സെർവർ ഔട്ട്പുട്ട് സ്ഥിരതയുള്ളതാക്കി നിലനിർത്തുകയും ചെയ്യുന്നു.

പെർഫോമൻസും മികച്ച രീതികളും (Performance and Best Practices)

useLayoutEffect പെയിന്റിംഗിനെ (painting) തടസ്സപ്പെടുത്തുന്നതിനാൽ, ഹുക്കിന്റെ ബോഡി എത്രത്തോളം ലളിതമാക്കാൻ കഴിയുമോ അത്രത്തോളം ലളിതമായി സൂക്ഷിക്കുക. ലേഔട്ട് വാല്യൂ വായിക്കുക, തിരുത്തലുകൾ കണക്കാക്കുക, അത് തിരികെ എഴുതുക. ഇതിനുള്ളിൽ ഡാറ്റ ഫെച്ച് ചെയ്യുകയോ, വലിയ ഒബ്ജക്റ്റുകൾ പാഴ്സ് ചെയ്യുകയോ, കഠിനമായ അൽഗോരിതങ്ങൾ പ്രവർത്തിപ്പിക്കുകയോ ചെയ്യരുത്. ഇവിടെയുള്ള കനത്ത കോഡുകൾ മെയിൻ ത്രെഡിനെ (main thread) തടസ്സപ്പെടുത്തുകയും നിങ്ങളുടെ ഇന്റർഫേസ് ഫ്രീസ് ആയതുപോലെ തോന്നിപ്പിക്കുകയും ചെയ്യും.

എലമെന്റുകൾ അളക്കുമ്പോൾ, document.getElementById-ന് പകരം React refs ഉപയോഗിക്കുക. Refs നിങ്ങളുടെ കമ്പോണന്റ് ഇൻസ്റ്റൻസുമായി ബന്ധപ്പെട്ടിരിക്കുന്നു, അവ റീ-റെൻഡറുകളിൽ തകരാറിലാകില്ല, കൂടാതെ പോർട്ടലുകളുമായോ (portals) കണ്ടിഷണൽ റെൻഡറിംഗുമായോ വിശ്വസനീയമായി പ്രവർത്തിക്കുന്നു. ഗ്ലോബൽ ID ലുക്കപ്പുകൾ കമ്പോണന്റ് എൻകാപ്സുലേഷൻ (encapsulation) തകർക്കുകയും നിങ്ങൾക്ക് ആവശ്യമുള്ള കൃത്യസമയത്ത് null നൽകുകയും ചെയ്തേക്കാം.

മിക്കവാറും എല്ലാ സൈഡ് ഇഫക്റ്റുകൾക്കും (side effect) അനുയോജ്യമായ ഡിഫോൾട്ട് useEffect ആണ്. ഇത് ബ്രൗസറിനെ തടസ്സമില്ലാതെ പെയിന്റ് ചെയ്യാൻ അനുവദിക്കുകയും ഡാറ്റ, ഇവന്റുകൾ, എക്സ്റ്റേണൽ സിൻക്രണൈസേഷൻ എന്നിവ വൃത്തിയായി കൈകാര്യം ചെയ്യുകയും ചെയ്യുന്നു. ഒരു പ്രത്യേക പ്രശ്നത്തിനുള്ള പ്രത്യേക ടൂളാണ് useLayoutEffect: പെയിന്റിംഗിന് മുമ്പ് ലേഔട്ട് വായിക്കുകയും തിരികെ എഴുതുകയും ചെയ്യുക. ഇവ തമ്മിലുള്ള സമയവ്യത്യാസം (timing difference) മനസ്സിലാക്കിയാൽ, സ്ക്രീനിലെ ഫ്ലിക്കറുകൾ (flickers) പരിഹരിക്കാൻ ശ്രമിക്കുന്നതിന് പകരം അവ ഒഴിവാക്കാൻ നിങ്ങൾക്ക് സാധിക്കും.

പ്രധാന കാര്യം: എല്ലാത്തിനും useEffect ഉപയോഗിച്ച് തുടങ്ങുക. ഒരു ടൂൾടിപ്പോ (tooltip) മോഡലോ (modal) ശരിയായ സ്ഥാനത്ത് എത്തുന്നതിന് മുമ്പ് തെറ്റായ സ്ഥലത്ത് മിന്നിമറയുന്നത് കാണുന്ന നിമിഷം, അത് നിങ്ങളുടെ സിഗ്നലാണ്. ഉടൻ തന്നെ useLayoutEffect-ലേക്ക് മാറുകയും, DOM അളക്കുകയും, ലേഔട്ട് ക്രമീകരിക്കുകയും ചെയ്യുക, തുടർന്ന് ബ്രൗസറിനെ കൃത്യമായി ഒരു തവണ മാത്രം പെയിന്റ് ചെയ്യാൻ അനുവദിക്കുക.