તમારું એપ દસ મિનિટ સુધી બરાબર ચાલે છે. પછી સ્ક્રોલિંગ ધીમું (sticky) થઈ જાય છે. અડધા કલાક પછી ટેબ એક ગીગાબાઈટ સુધી પહોંચી જાય છે. અંતે, આઉટ-ઓફ-મેમરી (out-of-memory) એરર સાથે પેજ બંધ થઈ જાય છે અને તમારી પાસે બતાવવા માટે કોઈ સ્ટેક ટ્રેસ (stack trace) હોતો નથી.
આ રેન્ડર પર્ફોર્મન્સ (render performance) ની સમસ્યા નથી. React DevTools Profiler શાંત દેખાશે કારણ કે સમસ્યા એ નથી કે ઘટકો (components) કેટલી વાર ફરીથી દોષ (redraw) થાય છે. સમસ્યા એ છે કે અનમાઉન્ટ (unmount) થયા પછી શું જીવંત રહે છે. JavaScript heap માં ક્યાંક રહેલો એક વધારાનો રેફરન્સ (stray reference) DOM નોડ્સ, ક્લોઝર્સ (closures) અને સ્ટેટ (state) ના આખા ટ્રીને પકડી રાખે છે. બ્રાઉઝર તેમાંથી કંઈપણ પાછું મેળવી શકતું નથી, તેથી પ્રોસેસ તૂટી ન પડે ત્યાં સુધી મેમરી વધતી જાય છે.
તમારો સોર્સ કોડ વાંચવાથી લીક (leak) વિશે ખબર નહીં પડે. આ બગ (bug) તમે જે વસ્તુ અનમાઉન્ટ થઈ ગઈ છે તેમ માનો છો અને ગાર્બેજ કલેક્ટર (garbage collector) ખરેખર જે જુએ છે તે વચ્ચેના અંતરમાં રહેલો છે. V8 ફક્ત એવા જ ઓબ્જેક્ટ્સને મુક્ત કરે છે જેમના રીટેઈનિંગ પાથ (retaining paths) શૂન્ય હોય. જો કોઈ અનિચ્છનીય ઇવેન્ટ લિસનર (event listener), ક્લિયર ન થયેલ ઓબ્ઝર્વર (observer), અથવા લાંબા સમય સુધી ચાલતા ક્લોઝર (closure) પાસે ફાઈબર (fiber) અથવા DOM નોડના માત્ર એક પોઈન્ટર પણ હોય, તો આખો કમ્પોનન્ટ સબટ્રી (component subtree) જીવંત રહે છે. તમે મોડલ (modal) અનમાઉન્ટ કરો છો, પરંતુ તેના ડિટેચ્ડ નોડ્સ (detached nodes) મેમરીમાં રહે છે કારણ કે window પરનો લિસનર હજુ પણ તે મોડલની અંદર વ્યાખ્યાયિત કરેલા હેન્ડલર તરફ નિર્દેશ કરે છે.
લીક ક્યાં છે તે સાબિત કરવા માટે તમારે એડિટરને નહીં, પણ હીપ (heap) ને જોવાની જરૂર છે.
હીપ (Heap) શા માટે સત્ય જણાવે છે
Chrome DevTools તમને ગાર્બેજ કલેક્ટર શું જુએ છે તેની સીધી જાણકારી આપે છે. Memory ટેબ હીપ સ્નેપશોટ્સ (heap snapshots) રેકોર્ડ કરી શકે છે: JavaScript મેમરીમાં હાલમાં રહેલા દરેક ઓબ્જેક્ટ, DOM નોડ અને ક્લોઝરની સંપૂર્ણ યાદી. બે સ્નેપશોટ્સની તુલના કરીને—એક શંકાસ્પદ લીક પહેલા અને એક પછી—તમે ચોક્કસપણે જાણી શકો છો કે કયા ઓબ્જેક્ટ્સ નાશ પામવામાં નિષ્ફળ રહ્યા છે.
આ કોઈ અમૂર્ત સિદ્ધાંત નથી. એક લીક થયેલ React કમ્પોનન્ટ હજારો ડિટેચ્ડ HTMLElement ઓબ્જેક્ટ્સને જાળવી રાખી શકે છે. તે ઓબ્જેક્ટ્સ હવે દેખાતા ડોક્યુમેન્ટ સાથે જોડાયેલા નથી, પરંતુ JavaScript રેફરન્સ તેમને કલેક્ટ થતા અટકાવે છે. તેઓ કમ્પેરીઝન વ્યુ (comparison view) માં Detached HTMLElement કન્સ્ટ્રક્ટર નામ સાથે દેખાય છે. જ્યારે તમે તેમને વધતા જુઓ છો, ત્યારે તમને તમારું લીક મળી જાય છે.
Chrome DevTools વર્કફ્લો (Workflow)
સાફ રીતે શરૂઆત કરો. બિનજરૂરી બ્રાઉઝર ટેબ્સ બંધ કરો, બિનજરૂરી એક્સટેન્શન ડિસેબલ કરો અને તમારી એપ્લિકેશનને સ્થિર સ્થિતિમાં આવવા દો. Chrome DevTools ખોલો, Memory ટેબ પર જાઓ અને Heap snapshot પસંદ કરો. Take snapshot પર ક્લિક કરો. આ બેઝલાઇન તમારા શરૂઆતના મેમરી ફૂટપ્રિન્ટને કેપ્ચર કરે છે.
હવે તમે જે યુઝર એક્શનનો શંકા કરો છો તે જ કરો. તે ભારે મોડલ ખોલો અને બંધ કરો. વિજેટ (widget) માઉન્ટ અને અનમાઉન્ટ કરો. રૂટ (route) પર જાઓ અને પાછા આવો. એકવાર UI તેની મૂળ વિઝ્યુઅલ સ્થિતિમાં આવી જાય પછી, Memory ટેબમાં કચરાપેટી (trash can) આઇકન પર ક્લિક કરો. આ ગ્લોબલ ગાર્બેજ કલેક્શન પાસ (garbage collection pass) માટે મજબૂર કરે છે. રેન્ડર સાયકલના કામચલાઉ ઓબ્જેક્ટ્સ દૂર થઈ જવા જોઈએ. જે કંઈ પણ બાકી રહે છે તે લીક માટેનો વાસ્તવિક ઉમેદવાર છે.
ફરીથી Take snapshot પર ક્લિક કરો. હવે તમારી પાસે મેમરીના બે ફોટોગ્રાફ છે. વ્યુને Summary થી બદલીને Comparison કરો. Comparison સ્કોપને પ્રથમ સ્નેપશોટ પર સેટ કરો. ટૂલ તમને ફક્ત તે જ બતાવશે જે બે કેપ્ચર વચ્ચે બદલાયું છે, જેનાથી રનટાઇમનો વધારાનો ઘોંઘાટ દૂર થશે.
Delta દ્વારા સોર્ટ કરો. વધેલા ઓબ્જેક્ટ કાઉન્ટ્સ શોધો. Detached HTMLElement, Array, Function, અથવા તમારા પોતાના કોડબેઝના નામવાળા ક્લાસ ઇન્સ્ટન્સ જેવા કન્સ્ટ્રક્ટર્સ પર ખાસ ધ્યાન આપો. વધતો ડેલ્ટા (delta) એટલે કે તમારા એક્શન દરમિયાન ઓબ્જેક્ટ્સ બનાવવામાં આવ્યા હતા અને તે પછી કલેક્ટ કરવામાં આવ્યા નહોતા.
રીટેઈનિંગ પાથ (Retaining Path) ટ્રેસ કરવો
જ્યારે તમે લીક થયેલ એલિમેન્ટ શોધો છો, ત્યારે તેને પસંદ કરો. નીચેનું પેનલ રીટેઈનિંગ પાથ દર્શાવે છે: રેફરન્સની એક ચેઈન જે સમજાવે છે કે આ ઓબ્જેક્ટ હજુ કેમ જીવંત છે. આ ચેઈન એક ડિટેચ્ડ div થી શરૂ થઈને React ના આંતરિક પ્રોપર્ટીઝ દ્વારા, ક્લોઝરમાં અને અંતે તમારા કમ્પોનન્ટ્સમાંના એકમાં રજિસ્ટર થયેલા ઇવેન્ટ લિસનર પર આવી શકે છે. ચેઈનમાં તે છેલ્લી લિંક તમારો લાઇન નંબર છે.
અહીં તમે નિદાન (diagnosis) થી મૂળ કારણ (root cause) તરફ આગળ વધો છો. જો રીટેઈનિંગ પાથ window.addEventListener પર સમાપ્ત થાય છે, તો તમે જાણો છો કે એક ગ્લોબલ લિસનર તમારા કમ્પોનન્ટને પકડી રાખે છે. જો તે IntersectionObserver ઇન્સ્ટન્સ પર સમાપ્ત થાય છે, તો તમે જાણો છો કે એક ઓબ્ઝર્વર હજુ પણ એવા નોડ પર નજર રાખી રહ્યો છે જેને ગાર્બેજ કલેક્ટ થવું જોઈતું હતું.
React માં સામાન્ય કારણો
React માં મેમરી લીક સામાન્ય રીતે ત્રણ પેટર્નમાં હોય છે.
અનાથ (Orphaned) ગ્લોબલ લિસનર્સ. useEffect સ્ક્રોલ પોઝિશન, કી પ્રેસ અથવા રિસાઇઝ ઇવેન્ટ્સને ટ્રેક કરવા માટે window અથવા document સાથે જોડાય છે. જો ઇફેક્ટ removeEventListener કોલ કરતી ક્લીનઅપ ફંક્શન (cleanup function) રિટર્ન કરતી નથી, તો લિસનર પેજના આયુષ્ય દરમિયાન જીવંત રહે છે. લિસનર એક ક્લોઝર હોવાથી, તે React દ્વારા કમ્પોનન્ટ અનમાઉન્ટ થયાના ઘણા સમય પછી પણ આખા કમ્પોનન્ટ સ્કોપને જીવંત રાખે છે.
ક્લીનઅપ ન થયેલા ઓબ્ઝર્વર્સ. IntersectionObserver અને ResizeObserver શક્તિશાળી છે, પરંતુ તેઓ React ના નિયંત્રણની બહાર નેટિવ રેફરન્સ બનાવે છે. જો તમે કોઈ કમ્પોનન્ટની અંદર ઓબ્ઝર્વર ઇન્સ્ટેન્શિયેટ કરો છો અને ક્લીનઅપ ફેઝમાં disconnect() કોલ કરવાનું ભૂલી જાઓ છો, તો ઓબ્ઝર્વર ટાર્ગેટ DOM નોડને પકડી રાખે છે, અને DOM નોડ React fibers, props, અને state ને પકડી રાખે છે.
ક્લોઝર ટ્રેપ્સ. જ્યારે તમે કમ્પોનન્ટની અંદર કોઈ ફંક્શન વ્યાખ્યાયિત કરો છો અને તેને થર્ડ-પાર્ટી લાઇબ્રેરી, ગ્લોબલ કેશ, અથવા setTimeout માં પાસ કરો છો, ત્યારે તે ફંક્શન તેના લેક્સિકલ સ્કોપમાં રહેલા દરેક વેરિયેબલ પર ક્લોઝર બનાવે છે. જો એક્સટર્નલ ઓનર તે ફંક્શનને રાખે છે, તો તે તેની સાથે તમારા આખા કમ્પોનન્ટ સ્કોપને પણ રાખે છે.
ખરેખર કામ કરતા ક્લીનઅપ પેટર્ન્સ
લીક (leak) સુધારવાનો અર્થ એ છે કે સ્નેપશોટમાં તમે શોધેલા દરેક રીટેઈનિંગ પાથ (retaining path) ને તોડી નાખવા.
useEffect માંથી હંમેશા ક્લીનઅપ ફંક્શન રિટર્ન કરો. જો તમે ઇફેક્ટમાં લિસનર ઉમેરો છો, તો તેને ત્યાં જ દૂર કરો.
DOM અથવા window સાથે તમે જોડતા કોઈપણ હેન્ડલર માટે useCallback નો ઉપયોગ કરો. તેના વગર, દરેક રેન્ડર એક નવો ફંક્શન રેફરન્સ બનાવે છે. જો તમે એક રેફરન્સ સાથે addEventListener કોલ કરો છો અને પછી બીજા અલગ રેફરન્સ સાથે removeEventListener કોલ કરો છો, તો તે દૂર કરવાની પ્રક્રિયા શાંતિથી નિષ્ફળ જાય છે. મૂળ લિસનર window પર કાયમ માટે રહી જાય છે. useCallback રેફરન્સને સ્ટેબલ રાખે છે જેથી add અને remove બરાબર મેચ થાય.
ઓબ્ઝર્વર્સને પણ તે જ શિસ્ત સાથે હેન્ડલ કરો. ઇફેક્ટની અંદર ઓબ્ઝર્વર ઇન્સ્ટન્સને ref અથવા લોકલ વેરિયેબલમાં સ્ટોર કરો. ક્લીનઅપ ફંક્શનમાં, observer.disconnect() કોલ કરો. એવું ન માનો કે કમ્પોનન્ટ અનમાઉન્ટ કરવાથી ઓબ્ઝર્વર ખતમ થઈ જશે. તે થતું નથી.
જો તમારો કમ્પોનન્ટ ગ્લોબલ નેમસ્પેસ અથવા સિંગલટન સર્વિસમાં કંઈપણ પબ્લિશ કરે છે, તો અનમાઉન્ટ વખતે તે રેફરન્સ ડિલીટ કરો. V8 એન્જિન મેમરી ત્યારે જ રિક્લેમ કરી શકે છે જ્યારે કોઈ ઓબ્જેક્ટ ખરેખર અનરીચેબલ હોય. window પર હૂક અથવા મોડ્યુલ-લેવલ Map માં એન્ટ્રી છોડવાથી એક અદ્રશ્ય બ્રિજ બને છે જે હીપને સતત વધારતો રાખે છે.
મુખ્ય વાત
મેમરી લીક તરત જ તમારા એપને ક્રેશ નથી કરતી. લાંબા યુઝર સેશન્સ દરમિયાન તે એક પછી એક ડિટેચ્ડ નોડ તરીકે જમા થતી જાય છે. તેનો ઉકેલ લાઇબ્રેરી અપગ્રેડ અથવા કમ્પાઈલર ફ્લેગ નથી. તે હીપ સ્નેપશોટ સાથે તમારા ક્લીનઅપ
