உங்கள் பயனரின் பிரவுசர் டேப் முப்பது நிமிடங்களுக்குப் பிறகு முடங்குகிறது. UI தடுமாறுகிறது. பின்னர் பிரவுசர் out-of-memory பிழையுடன் செயலிழக்கிறது.

நீங்கள் காம்போனென்ட் குறியீட்டை வரி வரியாக ஆய்வு செய்கிறீர்கள், ஆனால் அதில் எந்தத் தவறும் காணப்படவில்லை. React-இல் நினைவகக் கசிவுகள் (memory leaks) இருக்கும்போது இதுதான் மிகவும் எரிச்சலூட்டும் விஷயம். அந்தப் பிழை உங்கள் JSX syntax அல்லது உங்கள் hook logic-இல் இல்லை. அது உங்கள் காம்போனென்ட் மற்றும் பிரவுசரின் garbage collector ஆகியவற்றிற்கு இடையிலான இடைவெளியில் உள்ளது. ஒரு stray event listener அல்லது நீண்ட காலம் நீடிக்கும் closure, collector நினைவகத்தை மீட்டெடுப்பதைத் தடுக்கிறது. நீங்கள் ஒரு காம்போனென்ட்டை unmount செய்கிறீர்கள், ஆனால் ஒரு சிறிய lingering reference அந்த முழு மரத்தையும் (tree) heap-இல் உயிர்ப்புடன் வைத்திருக்கிறது. குறியீட்டைப் படிப்பதன் மூலம் மட்டும் இந்த கசிவுகளை உங்களால் கண்டுபிடிக்க முடியாது. இந்தப் பிரச்சனை மேற்பரப்பிற்கு அடியில் மறைந்திருக்கிறது, கண்ணுக்குத் தெரியாமலும் unit tests-இல் அமைதியாகவும் இருக்கிறது.

கசிவுகள் ஏன் கண்ணெதிரே மறைந்துள்ளன

V8 போன்ற JavaScript engines நினைவகத்தை தானாகவே நிர்வகிக்கின்றன. ஒரு object-லிருந்து root வரை எந்த reference path இல்லையென்றால், engine அந்த object-ஐ garbage என்று அடையாளப்படுத்தி அந்த இடத்தைத் திரும்பப் பெறுகிறது. நீங்கள் நினைத்ததை விட ஒரு மறைமுகமான reference நீண்ட காலம் நீடிக்கும் வரை இந்த செயல்முறை நன்றாகச் செயல்படும்.

React-இல், இந்த ஆபத்து பெரும்பாலும் காம்போனென்ட்கள் மற்றும் DOM ஆகியவற்றிற்கு இடையிலான எல்லையில் தோன்றுகிறது. ஒரு modal-க்குள் நீங்கள் window-வில் ஒரு resize listener-ஐ இணைக்கலாம், அல்லது ஒரு dashboard widget-இல் ஒரு WebSocket-ஐ subscribe செய்யலாம். பயனர் modal-ஐ மூடும்போது அல்லது வேறு பக்கத்திற்குச் செல்லும்போது, காம்போனென்ட் unmount ஆகிறது. அந்த subscription நீடித்தால், engine உலகளாவிய window object-லிருந்து உங்கள் handler வரை, மற்றும் உங்கள் handler-லிருந்து காம்போனென்ட்டின் closure வரை ஒரு செல்லுபடியாகும் reference இருப்பதை உணரும். அந்த காம்போனென்ட், அதன் props, அதன் state மற்றும் அதன் முழு subtree of DOM nodes ஆகிய அனைத்தும் நினைவகத்தில் நிலைநிறுத்தப்படுகின்றன (pinned). நூற்றுக்கணக்கான தொடர்புகளுக்குப் பிறகு, இந்த pinned objects தேக்கமடைகின்றன. நினைவகப் பயன்பாடு ஒரு sawtooth pattern-இல் உயர்ந்து கொண்டே இருக்கிறது, அது மீண்டும் முழுமையாகக் குறையவே இல்லை.

எண்டர்பிரைஸ் டேஷ்போர்டு சிக்கல்

பயனர்கள் ஒரு பக்கத்திலேயே பல மணிநேரம் இருக்கும் எண்டர்பிரைஸ் டேஷ்போர்டுகளில் இது அதிகம் நிகழ்கிறது. கண்காணிப்புப் பலகைகள் (monitoring panels), analytics views, அல்லது ticketing systems போன்றவற்றை நினைவில் கொள்ளுங்கள். ஒரு பயனர் ஒரு detail modal-ஐத் திறக்கிறார், ஒரு பெரிய dataset-ஐ filter செய்கிறார், அல்லது ஒரு single-page app-க்குள் டேப்களை மாற்றுகிறார். ஒவ்வொரு தனிப்பட்ட செயலும் சரியாகத் தோன்றலாம். ஆனால் காலப்போக்கில், அனாதையான (orphaned) nodes மற்றும் துண்டிக்கப்பட்ட (detached) listeners தேக்கமடைகின்றன. பயன்பாடு மெதுவாக மாறுவதற்கு ஒரு கடினமான render காரணமாக இருக்காது, மாறாக heap போதுமான அளவு வளர்ந்து அடிக்கடி மற்றும் அதிகச் செலவுமிக்க garbage collection pauses-களைத் தூண்டுவதாலேயே இது நிகழ்கிறது.

உங்கள் useEffect hooks பற்றி ஊகிப்பதை நிறுத்துங்கள். கசிவு இருக்கிறதா என்பதைத் தெரிந்துகொள்ள ஒரே வழி heap-ஐ நேரடியாக அளவிடுவதுதான். Chrome DevTools உங்களுக்கு அந்தத் தெளிவைத் தருகிறது.

Chrome DevTools மூலம் கசிவுகளைக் கண்டறிதல்

உங்களுக்கு மீண்டும் உருவாக்கக்கூடிய ஒரு வரிசைமுறை (reproducible sequence) மற்றும் சில நிமிடத் தீவிர கவனம் தேவை. உங்கள் பயன்பாட்டை Chrome-இல் திறந்து, DevTools-ஐத் தொடங்கி, Memory tab-க்குச் செல்லவும்.

ஒரு baseline-ஐப் பதிவு செய்யுங்கள். Heap snapshot என்பதைத் தேர்ந்தெடுத்து Take snapshot என்பதைக் கிளிக் செய்யவும். இது தற்போது JavaScript heap-இல் இருக்கும் ஒவ்வொரு object-ஐயும் பிடித்து, ஆரம்ப நினைவகத்தைக் காட்டுகிறது. இதை பக்கம் அதன் ஆரம்பக்கட்ட idle state-க்கு வந்த பிறகு செய்யவும், ஆரம்பப் பதிவின் (initial load) போது செய்ய வேண்டாம்; அப்போதுதான் பயனர் செயல்களால் ஏற்படும் வளர்ச்சியை மட்டுமே நீங்கள் அளவிட முடியும்.

செயலைத் தூண்டுங்கள். கசிவை ஏற்படுத்துவதாக நீங்கள் சந்தேகிக்கிற ஒரு துல்லியமான UI interaction-ஐச் செய்யவும். ஒரு modal-ஐத் திறந்து மூடவும், ஒரு சிக்கலான chart-ஐ toggle செய்யவும், அல்லது ஒரு route-ஐ மாற்றிவிட்டு மீண்டும் பழைய இடத்திற்கு வரவும். முடிந்ததும், பயன்பாட்டை அதன் அசல் காட்சி நிலைக்கு (original visual state) கொண்டு வரவும். இந்த படி மிக முக்கியமானது. UI காலியாகத் தெரிந்து, ஆனால் heap வளர்ந்திருந்தால், உங்களிடம் கசிவுக்கான வலுவான ஆதாரம் உள்ளது என்று அர்த்தம்.

Garbage collection-ஐக் கட்டாயப்படுத்துங்கள். Memory tab-இல் உள்ள குப்பைத் தொட்டி (trash can) ஐகானைக் கிளிக் செய்யவும். இது ஒரு முழுமையான GC cycle-ஐத் தூண்டி, அடுத்த collection வரை தற்காலிகமாகத் தங்கியிருக்கும் objects-களை நீக்கும். எஞ்சியிருப்பவைதான் உண்மையான கசிவுகள்; அதாவது சேகரிக்கப்பட வேண்டியிருந்தும், தற்செயலான references மூலம் உயிர்ப்புடன் வைக்கப்பட்டிருக்கும் objects.

இரண்டாவது snapshot-ஐ எடுங்கள். மீண்டும் Take snapshot என்பதைக் கிளிக் செய்யவும். இப்போது ஒரே மாதிரியான UI சூழலில் எடுக்கப்பட்ட heap-இன் இரண்டு புகைப்படங்கள் உங்களிடம் உள்ளன.

முடிவுகளை ஒப்பிடுங்கள். View-வை Summary என்பதிலிருந்து Comparison என்பதற்கு மாற்றவும். Baseline-ஆக உங்கள் முதல் snapshot-ஐயும், ஒப்பிட வேண்டிய snapshot-ஆக இரண்டாவது snapshot-ஐயும் அமைக்கவும். Comparison view ஒவ்வொரு object category-யையும் பட்டியலிட்டு, இரண்டு snapshot-களுக்கும் இடையிலான object எண்ணிக்கையின் நிகர மாற்றமான delta-வைக் காட்டுகிறது.

Delta மூலம் வரிசைப்படுத்துங்கள். கணிசமாக அதிகரித்துள்ள வகைகளைக் கவனியுங்கள். குறிப்பாக Detached HTMLElement மற்றும் React fiber nodes ஆகியவற்றில் கவனம் செலுத்துங்கள். ஒரு detached HTML element என்பது தற்போதுச் செயல்பாட்டில் உள்ள document tree-இல் இணைக்கப்படாத ஒரு DOM node ஆகும், ஆனால் ஏதோ ஒரு JavaScript reference இன்னும் அதைத் தக்கவைத்துள்ளது. இவைதான் மிகத் தெளிவான ஆதாரங்கள் (smoking guns). நீங்கள் ஒரு modal-ஐ மூடிய பிறகு அல்லது ஒரு காம்போனென்ட்டை unmount செய்த பிறகு இவை இருக்கக்கூடாது.

Retaining Path-ஐப் படித்தல்

நீங்கள் snapshot-இல் ஒரு detached element-ஐத் தேர்ந்தெடுக்கும்போது, Chrome அதன் bottom panel-இல் retaining path-ஐக் காட்டும். இந்த path என்பது root-லிருந்து தேர்ந்தெடுக்கப்பட்ட object வரை உள்ள references-களின் சங்கிலித் தொடராகும். அதை கவனமாகப் பின்தொடரவும். உங்கள் component-இல் உள்ள ஒரு குறிப்பிட்ட வரியைக் குறிக்கும் event listener, ஒரு IntersectionObserver, ஒரு setInterval ID அல்லது ஒரு closure ஆகியவற்றை நீங்கள் அடிக்கடி காணலாம்.

உங்களுக்குத் தெரிந்த பெயர்களைத் தேடுங்கள். உங்கள் codebase-இல் உள்ள ஒரு function பெயருடன் window-இல் இணைக்கப்பட்ட listener-ஐ நீங்கள் கண்டால், நீங்கள் anchor-ஐக் கண்டுபிடித்துவிட்டீர்கள் என்று அர்த்தம். அந்த listener-ஐத் தாங்கி நிற்கும் object உங்கள் முழு component-ஐயும் உயிர்ப்புடன் வைத்திருக்கிறது. சில நேரங்களில் இந்தச் சங்கிலித் தொடர் ஒரு third-party library வழியாகச் செல்லும். அத்தகைய சந்தர்ப்பங்களில், அந்த library ஒரு explicit teardown call-ஐ எதிர்பார்க்கிறதா என்றும், அதை நீங்கள் cleanup function-இல் அழைக்க மறந்துவிட்டீர்களா என்றும் சரிபார்க்கவும்.

மூல காரணங்களைத் தீர்த்தல்

நீங்கள் retaining path-ஐக் கண்டறிந்தவுடன், அதற்கான தீர்வு பொதுவாக இயந்திரத்தனமானது (mechanical), ஆனால் குழுவினரிடையே ஒழுக்கத்தைக் கோருகிறது.

cleanup functions-ஐப் பயன்படுத்தவும். நீங்கள் window அல்லது document-இல் listeners-களைச் சேர்க்கும்போது, எப்போதும் useEffect-இல் ஒரு cleanup function-ஐ return செய்யவும். உங்கள் effect ஒரு resize event-இல் subscribe செய்யப்பட்டால், component unmount ஆவதற்கு முன் அந்த subscription-ஐ நீக்கிவிடவும். React component-ஐ tear down செய்யும்போது cleanup இயங்கும், இது வெளிப்புற இணைப்புகளைத் துண்டிக்க உங்களுக்கு ஒரு உறுதியான hook-ஐ வழங்குகிறது.

references-களை நிலைப்படுத்தவும் (Stabilize). உங்கள் handlers-களை useCallback-இல் சுற்றவும் (wrap). இதன் மூலம், நீங்கள் முதலில் addEventListener-க்கு வழங்கிய அதே function reference-ஐயே removeEventListener-க்கும் வழங்குகிறீர்கள் என்பதை இது உறுதி செய்கிறது. நீங்கள் window.addEventListener('resize', () => { ... }) போன்ற ஒரு inline function-ஐப் பதிவு செய்துவிட்டு, பின்னர் வேறொரு inline function மூலம் அதை நீக்க முயன்றால், references பொருந்தாது. listener இணைக்கப்பட்டே இருக்கும். அதற்குள் இருக்கும் closure உங்கள் component state-ஐ உயிர்ப்புடன் வைத்திருக்கும். ஒரு stable dependency array-உடன் கூடிய useCallback இந்த identity mismatch-ஐத் தடுக்கிறது.

global links-களைத் துண்டிக்கவும். window மற்றும் document போன்ற browser-இன் event target objects, அந்தப் பக்கத்தின் ஆயுட்காலம் முழுவதும் இருக்கும் என்பதை நினைவில் கொள்ளுங்கள். அவற்றிலிருந்து உங்கள் component-க்குள் செல்லும் எந்தவொரு reference-உம் ஒரு global anchor போலச் செயல்படும். listener-ஐ நீக்குவது அந்த anchor-ஐ உடைத்துவிடும், மேலும் அடுத்த garbage collection cycle-இன் போது V8 engine component state மற்றும் DOM nodes-களை அகற்றிவிட அனுமதிக்கும்.

window width-ஐக் கண்காணிக்கும் ஒரு modal-ஐக் கருத்தில் கொள்ளுங்கள். cleanup இல்லையென்றால், பயனர் ஒவ்வொரு முறை modal-ஐத் திறக்கும் போதும், ஒரு புதிய listener இணையும். அவை சார்ந்த component instances மறைந்துவிட்டாலும், அந்த functions anonymous ஆகவும் காணாமல் போனதாகவும் இருப்பதால், பழைய listener-கள் ஒருபோதும் நீக்கப்படாது. useCallback-இல் சுற்றப்பட்ட ஒரு named handler மற்றும் removeEventListener-ஐ அழைக்கும் ஒரு cleanup function ஆகியவற்றைப் பயன்படுத்துவது இந்தச் சுழற்சியைத் துல்லியமாக முடிவுக்குக் கொண்டுவரும்.

ஒரு முக்கியப் பாடம்

React-இல் memory leaks அரிதாகவே தெளிவான error message-களுடன் வெளிப்படும். அவை எவ்வளவு நேரம் திறந்திருக்கிறதோ அவ்வளவு அதிகமாகச் சுமையாகும் (heavier) ஒரு tab மூலம் தங்களை வெளிப்படுத்தும். எந்த hook குற்றவாளி என்று ஊகிப்பதில் நேரத்தை வீணாக்காதீர்கள். Memory tab-ஐத் திறந்து, garbage collection-ஐத் தூண்டி (force), snapshots-களை ஒப்பிட்டுப் பார்க்கவும். heap profiler மூலம் துல்லியமான retaining path-ஐக் கண்டறியுங்கள். பின்னர் cleanup function-ஐ எழுதவும், callback reference-ஐ நிலைப்படுத்தவும் மற்றும் global link-ஐத் துண்டிக்கவும். உங்கள் பயனர்கள் இந்தத் தீர்வை நேரடியாகக் கவனிக்க மாட்டார்கள், ஆனால் நாள் முடிவிலும் dashboard தடையின்றி இயங்குவதை அவர்கள் உணர்வார்கள்.