તમારા વપરાશકર્તાનું બ્રાઉઝર ટેબ ત્રીસ મિનિટ પછી ફ્રીઝ થઈ જાય છે. UI અટકી જાય છે. પછી બ્રાઉઝર out-of-memory error સાથે ક્રેશ થઈ જાય છે.
તમે કમ્પોનન્ટ કોડને લાઇન બાય લાઇન રિવ્યૂ કરો છો અને તેમાં કંઈ ખોટું દેખાતું નથી. React માં મેમરી લીક્સ (memory leaks) એ આ જ પાગલ કરી દેનારી બાબત છે. આ બગ તમારી JSX સિન્ટેક્સ અથવા તમારા hook લોજિકમાં નથી. તે તમારા કમ્પોનન્ટ અને બ્રાઉઝરના garbage collector વચ્ચેના અંતરાલમાં રહેલો છે. એક વધારાનો event listener અથવા લાંબા સમય સુધી ચાલતું closure કલેક્ટરને મેમરી પાછી મેળવતા અટકાવે છે. તમે કમ્પોનન્ટને unmount કરો છો, પરંતુ એક જ બાકી રહેલું રેફરન્સ આખા ટ્રીને heap માં જીવંત રાખે છે. તમે ફક્ત કોડ વાંચીને આ લીક્સ શોધી શકતા નથી. સમસ્યા સપાટીની નીચે છુપાયેલી રહે છે, જે આંખે દેખાતી નથી અને યુનિટ ટેસ્ટમાં શાંત રહે છે.
લીક્સ શા માટે દેખીતી રીતે છુપાયેલા રહે છે
V8 જેવા JavaScript engines મેમરીનું સંચાલન આપમેળે કરે છે. જ્યારે રૂટથી કોઈ ઓબ્જેક્ટ સુધી કોઈ રેફરન્સ પાથ ન હોય, ત્યારે એન્જિન તે ઓબ્જેક્ટને garbage તરીકે માર્ક કરે છે અને તે જગ્યા ખાલી કરી દે છે. આ પ્રક્રિયા ત્યાં સુધી સારી રીતે કામ કરે છે જ્યાં સુધી કોઈ છુપાયેલું રેફરન્સ તમે ઈચ્છો છો તેના કરતા વધુ સમય સુધી ટકી ન રહે.
React માં, જોખમ ઘણીવાર કમ્પોનન્ટ્સ અને DOM વચ્ચેની સીમા પર દેખાય છે. તમે મોડલ (modal) ની અંદર window પર resize listener લગાવી શકો છો, અથવા ડેશબોર્ડ વિજેટમાં WebSocket ને સબ્સ્ક્રાઇબ કરી શકો છો. જ્યારે વપરાશકર્તા મોડલ બંધ કરે છે અથવા નેવિગેટ કરે છે, ત્યારે કમ્પોનન્ટ unmount થાય છે. જો સબ્સ્ક્રિપ્શન ટકી રહે છે, તો એન્જિન ગ્લોબલ window ઓબ્જેક્ટથી તમારા હેન્ડલર સુધી અને તમારા હેન્ડલરથી કમ્પોનન્ટના closure સુધી એક માન્ય રેફરન્સ જુએ છે. કમ્પોનન્ટ, તેના props, તેની state, અને તેના DOM nodes નું આખું subtree મેમરીમાં પિન (pinned) રહે છે. સેંકડો ઇન્ટરેક્શન પછી, આ પિન થયેલા ઓબ્જેક્ટ્સ જમા થવા લાગે છે. મેમરીનો વપરાશ sawtooth pattern માં વધે છે જે ક્યારેય સંપૂર્ણપણે નીચે આવતું નથી.
એન્ટરપ્રાઇઝ ડેશબોર્ડની સમસ્યા
આ સૌથી વધુ એન્ટરપ્રાઇઝ ડેશબોર્ડ્સમાં થાય છે જ્યાં વપરાશકર્તાઓ કલાકો સુધી એક જ પેજ પર રહે છે. મોનિટરિંગ પેનલ્સ, એનાલિટિક્સ વ્યુઝ અથવા ટિકિટિંગ સિસ્ટમ્સ વિશે વિચારો. વપરાશકર્તા એક ડિટેલ મોડલ ખોલે છે, મોટા ડેટાસેટને ફિલ્ટર કરે છે અથવા સિંગલ-પેજ એપમાં ટેબ બદલે છે. દરેક વ્યક્તિગત ઇન્ટરેક્શન બરાબર લાગે છે. જોકે, સમય જતાં, અનાથ (orphaned) nodes અને અલગ થયેલા (detached) listeners જમા થાય છે. એપ્લિકેશન કોઈ એક મોંઘા render ને કારણે નહીં, પરંતુ heap એટલું મોટું થઈ જાય છે કે વારંવાર અને ખર્ચાળ garbage collection pauses ટ્રિગર થાય છે, તેના કારણે ધીમી પડે છે.
તમારા useEffect hooks વિશે અનુમાન કરવાનું બંધ કરો. લીક છે કે નહીં તે જાણવાનો એકમાત્ર રસ્તો heap ને સીધું માપવાનો છે. Chrome DevTools તમને તે વિઝિબિલિટી આપે છે.
Chrome DevTools સાથે લીક્સ શોધવી
તમારે એક પુનરાવર્તિત કરી શકાય તેવી (reproducible) શ્રેણી અને થોડી મિનિટોના કેન્દ્રિત ધ્યાનની જરૂર છે. Chrome માં તમારી એપ્લિકેશન ખોલો, DevTools શરૂ કરો અને Memory ટેબ પર જાઓ.
બેઝલાઇન રેકોર્ડ કરો. Heap snapshot પસંદ કરો અને Take snapshot પર ક્લિક કરો. આ હાલમાં JavaScript heap માં જીવંત હોય તેવા દરેક ઓબ્જેક્ટને કેપ્ચર કરે છે અને તમને શરૂઆતની મેમરી બતાવે છે. આ પ્રક્રિયા પેજ તેના પ્રારંભિક idle સ્ટેટમાં સ્થિર થયા પછી કરો, શરૂઆતના લોડ દરમિયાન નહીં, જેથી તમે ફક્ત વપરાશકર્તાની ક્રિયાઓ દ્વારા થતા વધારાને જ માપી શકો.
એક્શન ટ્રિગર કરો. તમે જે UI ઇન્ટરેક્શનને લીકનું કારણ માનો છો તે જ કરો. એક મોડલ ખોલો અને બંધ કરો, એક જટિલ ચાર્ટને ટોગલ કરો, અથવા રૂટ બદલો અને પાછા નેવિગેટ કરો. પૂરું થયા પછી, એપને તેની મૂળ વિઝ્યુઅલ સ્થિતિમાં પરત લાવો. આ સ્ટેપ અત્યંત મહત્વપૂર્ણ છે. તમે ઈચ્છો છો કે UI બરાબર એવું જ દેખાય જેવું બેઝલાઇન દરમિયાન દેખાતું હતું. જો UI ખાલી દેખાવા છતાં heap વધ્યું હોય, તો તમારી પાસે લીકનો મજબૂત પુરાવો છે.
Garbage collection ને ફોર્સ કરો. Memory ટેબમાં કચરાપેટી (trash can) આઇકન પર ક્લિક કરો. આ એક સંપૂર્ણ GC સાયકલ ટ્રિગર કરે છે અને કામચલાઉ ઓબ્જેક્ટ્સને ક્લિયર કરે છે જે કાયદેસર રીતે આગામી કલેક્શન સુધી ટકી રહ્યા હતા. જે બાકી રહે છે તે વાસ્તવિક લીક્સ છે, એવા ઓબ્જેક્ટ્સ જે કલેક્ટ થઈ જવા જોઈતા હતા પરંતુ અકસ્માતવશ રેફરન્સ દ્વારા જીવંત રાખવામાં આવ્યા હતા.
બીજું snapshot લો. ફરીથી Take snapshot પર ક્લિક કરો. હવે તમારી પાસે સમાન UI સ્થિતિ હેઠળ લેવામાં આવેલા heap ના બે ફોટોગ્રાફ છે.
પરિણામોની સરખામણી કરો. વ્યુને Summary થી બદલીને Comparison કરો. બેઝલાઇનને તમારા પ્રથમ snapshot પર અને સરખામણી કરવા માટેના snapshot ને બીજા પર સેટ કરો. Comparison વ્યુ દરેક ઓબ્જેક્ટ કેટેગરીની યાદી આપે છે અને તમને ડેલ્ટા (delta) બતાવે છે, જે બે કેપ્ચર વચ્ચે ઓબ્જેક્ટ કાઉન્ટમાં થયેલ ચોખ્ખો ફેરફાર છે.
Delta દ્વારા સોર્ટ કરો. જે કેટેગરીમાં નોંધપાત્ર વધારો થયો હોય તે જુઓ. ખાસ કરીને Detached HTMLElement અને React fiber nodes પર ધ્યાન કેન્દ્રિત કરો. Detached HTML element એ એક DOM node છે જે હવે એક્ટિવ ડોક્યુમેન્ટ ટ્રી સાથે જોડાયેલ નથી, છતાં કોઈ JavaScript રેફરન્સ હજુ પણ તેને પકડી રાખે છે. આ નિશ્ચિત પુરાવા (smoking guns) છે. મોડલ બંધ કર્યા પછી અથવા કમ્પોનન્ટ unmount કર્યા પછી તે અસ્તિત્વમાં ન હોવા જોઈએ.
Retaining Path વાંચવું
જ્યારે તમે સ્નેપશોટમાં અલગ થયેલ (detached) એલિમેન્ટ પસંદ કરો છો, ત્યારે Chrome નીચેના પેનલમાં રીટેઈનિંગ પાથ (retaining path) બતાવે છે. આ પાથ રૂટથી લઈને પસંદ કરેલ ઓબ્જેક્ટ સુધીના રેફરન્સની એક સાંકળ છે. તેને ધ્યાનથી અનુસરો. તમને ઘણીવાર તમારા કમ્પોનન્ટની કોઈ ચોક્કસ લાઇન તરફ નિર્દેશ કરતો ઇવેન્ટ લિસનર (event listener), IntersectionObserver, setInterval ID, અથવા ક્લોઝર (closure) જોવા મળશે.
તમે ઓળખી શકો તેવા નામો શોધો. જો તમે તમારા કોડબેઝમાંથી કોઈ ફંક્શન નામ સાથે window સાથે જોડાયેલ લિસનર જુઓ છો, તો તમે એન્કર (anchor) શોધી લીધું છે. તે લિસનરને પકડી રાખતો ઓબ્જેક્ટ તમારા સમગ્ર કમ્પોનન્ટને જીવંત રાખી રહ્યો છે. ક્યારેક આ સાંકળ થર્ડ-પાર્ટી લાઇબ્રેરી દ્વારા ચાલે છે. તેવા કિસ્સાઓમાં, તપાસો કે શું લાઇબ્રેરી કોઈ સ્પષ્ટ (explicit) teardown call ની અપેક્ષા રાખે છે જે તમે ક્લીનઅપ ફંક્શનમાં ઇવોક (invoke) કરવાનું ભૂલી ગયા હોવ.
મૂળ કારણોનું નિવારણ
એકવાર તમે રીટેઈનિંગ પાથ ઓળખી લો, પછી તેનું નિવારણ સામાન્ય રીતે મિકેનિકલ હોય છે પરંતુ તેના માટે ટીમમાં શિસ્તની જરૂર હોય છે.
ક્લીનઅપ ફંક્શન્સનો ઉપયોગ કરો. જ્યારે તમે window અથવા document માં લિસનર્સ ઉમેરો છો, ત્યારે હંમેશા useEffect માં ક્લીનઅપ ફંક્શન રિટર્ન કરો. જો તમારો ઇફેક્ટ (effect) resize ઇવેન્ટને સબ્સ્ક્રાઇબ કરે છે, તો કમ્પોનન્ટ અનમાઉન્ટ (unmount) થાય તે પહેલાં તે સબ્સ્ક્રિપ્શન દૂર કરો. જ્યારે React કમ્પોનન્ટને તોડી નાખે (tears down) છે ત્યારે ક્લીનઅપ ચાલે છે, જે તમને બાહ્ય કનેક્શન તોડવા માટે એક ગેરંટીડ હૂક (hook) આપે છે.
રેફરન્સને સ્ટેબિલાઇઝ કરો. તમારા હેન્ડલર્સને useCallback માં રેપ (wrap) કરો. આ સુનિશ્ચિત કરે છે કે તમે removeEventListener ને બરાબર એ જ ફંક્શન રેફરન્સ પાસ કરો છો જે તમે મૂળરૂપે addEventListener ને પાસ કર્યો હતો. જો તમે window.addEventListener('resize', () => { ... }) જેવું ઇનલાઇન ફંક્શન રજિસ્ટર કરો છો અને પછી તેને બીજા ઇનલાઇન ફંક્શનથી દૂર કરવાનો પ્રયાસ કરો છો, તો રેફરન્સ મેચ થશે નહીં. લિસનર જોડાયેલ રહેશે. તેની અંદરનું ક્લોઝર તમારા કમ્પોનન્ટ સ્ટેટને જીવંત રાખશે. સ્ટેબલ ડિપેન્ડન્સી એરે (dependency array) સાથેનું useCallback આ આઇડેન્ટિટી મિસમેચને અટકાવે છે.
ગ્લોબલ લિંક્સ તોડો. યાદ રાખો કે બ્રાઉઝરના ઇવેન્ટ ટાર્ગેટ ઓબ્જેક્ટ્સ, જેમ કે window અને document, પેજના આયુષ્ય દરમિયાન જીવંત રહે છે. તેમનાથી તમારા કમ્પોનન્ટમાં કોઈપણ રેફરન્સ ગ્લોબલ એન્કર તરીકે કામ કરે છે. લિસનર દૂર કરવાથી તે એન્કર તૂટી જાય છે અને V8 એન્જિનને આગામી ગાર્બેજ કલેક્શન સાયકલ દરમિયાન કમ્પોનન્ટ સ્ટેટ અને DOM નોડ્સને સાફ કરવા દે છે.
વિન્ડો વિડ્થ (window width) ટ્રેક કરતા મોડલ (modal) નો વિચાર કરો. ક્લીનઅપ વગર, જ્યારે પણ યુઝર મોડલ ખોલે છે, ત્યારે નવું લિસનર જોડાય છે. જૂના લિસનર્સ ક્યારેય અલગ થતા નથી કારણ કે તેઓ જે કમ્પોનન્ટ ઇન્સ્ટન્સના છે તે અસ્તિત્વમાં નથી, છતાં ફંક્શન્સ પોતે અનામી (anonymous) હતા અને ખોવાઈ ગયા હતા. useCallback માં રેપ કરેલ નેમ્ડ હેન્ડલર (named handler) નો ઉપયોગ કરવાથી, અને removeEventListener ને કોલ કરતા ક્લીનઅપ ફંક્શન સાથે, આ લૂપ ચોખ્ખી રીતે બંધ થઈ જાય છે.
વાસ્તવિક બોધપાઠ
React માં મેમરી લીક (Memory leaks) ભાગ્યે જ સ્પષ્ટ એરર મેસેજ સાથે પોતાની જાતને જાહેર કરે છે. તેઓ એવા ટેબ દ્વારા પોતાની જાતને જાહેર કરે છે જે જેટલું લાંબુ ખુલ્લું રહેશે તેટલું ભારે થતું જશે. કયો હૂક દોષિત છે તે વિશે અનુમાન લગાવવામાં સમય ન બગાડો. Memory ટેબ ખોલો, ગાર્બેજ કલેક્શન (garbage collection) માટે ફોર્સ કરો અને સ્નેપશોટ્સની સરખામણી કરો. Heap profiler ને તમને ચોક્કસ રીટેઈનિંગ પાથ બતાવવા દો. પછી ક્લીનઅપ ફંક્શન લખો, કોલબેક રેફરન્સને સ્ટેબિલાઇઝ કરો અને ગ્લોબલ લિંક કાપી નાખો. તમારા યુઝર્સ સીધું જ આ ફિક્સ નોંધશે નહીં, પરંતુ તેઓ નોંધશે કે દિવસના અંતે પણ ડેશબોર્ડ હજુ પણ સ્મૂધલી ચાલે છે.
