Programu yako inafanya kazi vizuri kwa dakika kumi. Kisha kusogeza ukurasa (scroll) kunakuwa kizito. Baada ya nusu saa, tab inafikia gigabyte moja. Hatimaye ukurasa unakufa kwa kosa la "out-of-memory" na huna hata "stack trace" ya kuonyesha.
Hili si tatizo la utendaji wa uchoraji (render performance). React DevTools Profiler itaonekana imetulia kwa sababu tatizo si jinsi mara kwa mara vipengee (components) vinavyochorwa upya. Tatizo ni kile kinachobaki hai baada ya kuondolewa (unmount). Marejeo (reference) yaliyotapakaa mahali fulani kwenye JavaScript heap yanashikilia mti mzima wa DOM nodes, closures, na state. Kivinjari (browser) hakiwezi kurejesha chochote kati ya hivyo, hivyo kumbukumbu (memory) inaongezeka hadi mchakato unapoanguka.
Kusoma msimbo wako wa chanzo (source code) hakutafichua uvujaji huo. Hitilafu (bug) ipo kwenye pengo kati ya kile unachofikiri kimeondolewa (unmounted) na kile ambacho "garbage collector" inaona hasa. V8 huachilia tu vitu ambavyo vina njia sifuri za kuhifadhi (retaining paths). Ikiwa "event listener" isiyofanyiwa kazi, "observer" ambayo haijasafishwa, au "closure" inayodumu kwa muda mrefu inashikilia hata kiunganishi (pointer) kimoja kwenye "fiber" au "DOM node", mti mzima wa vipengee (component subtree) utabaki hai. Unaondoa modal, lakini "detached nodes" zake zinabaki kwenye kumbukumbu kwa sababu "listener" kwenye window bado inaelekeza kwenye "handler" iliyofafanuliwa ndani ya modal hiyo.
Ili kuthibitisha mahali uvujaji ulipo, unahitaji kuangalia "heap", siyo "editor".
Kwa Nini Heap Inasema Ukweli
Chrome DevTools inakupa dirisha la moja kwa moja la kile ambacho "garbage collector" inaona. Tab ya Memory inaweza kurekodi "heap snapshots": orodha kamili ya kila kitu (object), DOM node, na closure inayoshikiliwa sasa hivi kwenye kumbukumbu ya JavaScript. Kwa kulinganisha "snapshots" mbili—moja kabla ya uvujaji unaohisiwa na nyingine baada yake—unaweza kutenga sawia ni vitu gani vilivyoshindwa kufutika.
Hii si nadharia ya kinadharia. Kipengee kimoja cha React kilichovuja kinaweza kuhifadhi maelfu ya vitu vya HTMLElement vilivyojitenga (detached). Vitu hivyo havijaunganishwa tena kwenye hati (document) inayoonekana, lakini marejeo ya JavaScript yanazuia kukusanywa kwake. Huonekana kwenye mtazamo wa kulinganisha (comparison view) kwa jina la "constructor" Detached HTMLElement. Unapoona vikiongezeka, umepata uvujaji wako.
Mtiririko wa Kazi wa Chrome DevTools
Anza kwa usafi. Funga tab za kivinjari zisizohusika, zima "extensions" zisizohusika, na uache programu yako itulie katika hali thabiti. Fungua Chrome DevTools, badilisha kwenda kwenye tab ya Memory, na uchague Heap snapshot. Bonyeza Take snapshot. Msingi huu unarekodi kiasi cha kumbukumbu unapoanza.
Sasa fanya kitendo cha mtumiaji unachohisi kinasababisha tatizo. Fungua na ufunge modal hiyo nzito. Weka (mount) na uondoa (unmount) widget. Nenda kwenye njia (route) na urudi. Mara tu UI inaporejea katika hali yake ya awali ya kuonekana, bonyeza ikoni ya pipa la taka kwenye tab ya Memory. Hii inalazimisha mchakato wa jumla wa "garbage collection". Vitu vya muda kutoka kwenye mzunguko wa uchoraji (render cycle) vinapaswa kuondolewa. Chochote kinachobaki ni mshukiwa halisi wa uvujaji.
Bonyeza Take snapshot tena. Sasa unayo picha mbili za kumbukumbu. Badilisha mtazamo kutoka Summary kwenda Comparison. Weka upeo wa kulinganisha (comparison scope) kwenye snapshot ya kwanza. Zana itakuonyesha tu kile kilichobadilika kati ya picha hizo mbili, ikiondoa kelele za wakati wa utendaji (runtime).
Panga kwa Delta. Angalia idadi ya vitu ambazo zimeongezeka. Toa umakini wa pekee kwa "constructors" kama Detached HTMLElement, Array, Function, au hata mifano ya madarasa (class instances) yenye majina kutoka kwenye msimbo wako. Delta inayoongezeka inamaanisha vitu viliumbwa wakati wa kitendo chako na havikuondolewa baadaye.
Kufuatilia Njia ya Kuhifadhi (Retaining Path)
Unapobaini kipengele kilichovuja, kishughulikie (select). Paneli ya chini inaonyesha njia ya kuhifadhi (retaining path): mnyororo wa marejeo unaoelezea kwa nini kitu hiki bado kipo hai. Mnyororo huo unaweza kuanzia kwenye div iliyotengana kupitia sifa za ndani za React, kuingia kwenye "closure", na hatimaye kumalizia kwenye "event listener" iliyosajiliwa ndani ya moja ya vipengee vyako. Kiungo hicho cha mwisho katika mnyororo ndicho namba yako ya mstari (line number).
Hapa ndipo unapoanzia kutoka kwenye utambuzi hadi kwenye chanzo cha tatizo. Ikiwa njia ya kuhifadhi inaishia kwenye window.addEventListener, unajua kuwa "listener" ya kimataifa inashikilia kipengee chako mateka. Ikiwa inaishia kwenye mfano wa IntersectionObserver, unajua kuwa "observer" bado inatazama "node" ambayo ingepaswa kufutwa na "garbage collector".
Wahusika Wakuu katika React
Uvujaji wa kumbukumbu katika React kwa kawaida hugawanyika katika mifumo mitatu.
Orphaned global listeners. useEffect inaunganishwa na window au document ili kufuatilia nafasi ya kusogeza (scroll position), shinikizo la funguo, au matukio ya kubadilisha ukubwa (resize events). Ikiwa athari (effect) hairudishi kazi ya usafishaji (cleanup function) inayoiita removeEventListener, msikilizaji huo huendelea kuishi kwa muda wote wa ukurasa. Kwa sababu msikilizaji huyo ni "closure", unaweka mzunguko mzima wa kipengee (component scope) hai kwa muda mrefu baada ya React kuondoa kipengee hicho.
Uncleared observers. IntersectionObserver and ResizeObserver are powerful, but they create native references outside React’s control. If you instantiate an observer inside a component and forget to call disconnect() in the cleanup phase, the observer holds the target DOM node, and the DOM node holds React fibers, props, and state.
Closure traps. When you define a function inside a component and pass it to a third-party library, a global cache, or even setTimeout, that function closes over every variable in its lexical scope. If the external owner keeps the function around, it keeps your whole component scope around with it.
Cleanup Patterns That Actually Work
Fixing a leak means severing every retaining path you found in the snapshot.
Always return a cleanup function from useEffect. If you add a listener in the effect, remove it there.
Use useCallback for any handler you attach to the DOM or window. Without it, every render creates a new function reference. If you call addEventListener with one reference and later call removeEventListener with a different one, the removal fails silently. The original listener stays on window forever. useCallback keeps the reference stable so that add and remove match exactly.
Handle observers with the same discipline. Store the observer instance in a ref or local variable inside the effect. In the cleanup function, call observer.disconnect(). Do not assume unmounting the component kills the observer. It does not.
If your component publishes anything to a global namespace or a singleton service, delete those references on unmount. The V8 engine can only reclaim memory when an object is truly unreachable. Leaving a hook on window or an entry in a module-level Map creates an invisible bridge that keeps the heap growing.
The Real Takeaway
Memory leaks do not crash your app immediately. They accumulate one detached node at a time during long user sessions. The fix is not a library upgrade or a compiler flag. It is the habit of proving your cleanup logic with heap snapshots.
Take a baseline, trigger the suspect flow, force garbage collection, and compare. If the delta shows growth, inspect the retaining path, find the listener or observer that should not exist, and cut the reference. Run the test again. When the delta stays flat, you have actually solved it. Your application will stay responsive, and your users will not lose their work to a frozen browser tab.
