Tab ya kivinjari ya mtumiaji wako inaganda baada ya dakika tatu. UI inasita-sita. Kisha kivinjari kinazimika kwa kosa la 'out-of-memory'.

Unapitia kodi ya component mstari kwa mstari na huoni chochote kibaya. Hiyo ndiyo sehemu inayochanganya ya uvujaji wa kumbukumbu (memory leaks) katika React. Hitilafu haipo katika sintaksi yako ya JSX au mantiki ya hook yako. Ipo katika pengo kati ya component yako na 'garbage collector' wa kivinjari. 'Event listener' iliyopotea au 'closure' inayodumu kwa muda mrefu inazuia 'collector' isipate tena kumbukumbu. Unatoa component (unmount), lakini rejea moja inayobaki inaweka mti mzima (tree) hai kwenye 'heap'. Huwezi kupata uvujaji huu kwa kusoma kodi tu. Tatizo linabaki limejificha chini ya uso, halionekani kwa macho na halisikiki kwenye majaribio ya unit tests.

Kwa Nini Uvujaji Unajificha Machoni Pote

Injini za JavaScript kama V8 husimamia kumbukumbu kiotomatiki. Wakati hakuna njia ya rejea (reference path) kutoka kwenye mzizi (root) hadi kwenye kitu (object), injini huainisha kitu hicho kama takataka (garbage) na kurejesha nafasi hiyo. Mchakato huu unafanya kazi vizuri hadi rejea iliyojificha inapodumu kwa muda mrefu zaidi ya ulivyokusudia.

Katika React, hatari mara nyingi hutokea kwenye mpaka kati ya components na DOM. Unaweza kuunganisha resize listener kwenye window ndani ya modal, au kujiunga na WebSocket kwenye widget ya dashboard. Mtumiaji anapofunga modal au kuondoka, component inatoa (unmounts). Ikiwa usajili (subscription) utadumu, injini itaona rejea halali kutoka kwenye kitu cha kimataifa cha window hadi kwenye 'handler' yako, na kutoka kwenye 'handler' yako kurudi kwenye 'closure' ya component. Component, props zake, hali (state) yake, na mti wake mzima wa node za DOM unabaki yamefungwa kwenye kumbukumbu. Kupitia mwingiliano mamia, vitu hivi vilivyofungwa hujikusanya. Matumizi ya kumbukumbu hupanda kwa mfumo wa 'sawtooth' ambao haushuki kabisa.

Tatizo la Dashboard za Mashirika

Hii hutokea zaidi katika dashboard za mashirika ambapo watumiaji hukaa kwenye ukurasa mmoja kwa saa nyingi. Fikiria paneli za ufuatiliaji, mitazamo ya uchambuzi (analytics), au mifumo ya tiketi. Mtumiaji anafungua modal ya maelezo, anachuja seti kubwa ya data, au anabadilisha tab ndani ya programu ya ukurasa mmoja (single-page app). Kila mwingiliano wa mtu mmoja unaonekana kuwa sawa. Hata hivyo, baada ya muda, node zilizosalia bila mwenye (orphaned nodes) na 'listeners' zilizojitenga (detached listeners) hujikusanya. Programu inasonga polepole si kwa sababu ya 'render' moja ghali, bali kwa sababu 'heap' inakuwa kubwa kiasi cha kusababisha kusimama kwa mara kwa mara kwa 'garbage collection' ambako kunagharimu muda.

Acha kukisia kuhusu useEffect hooks zako. Njia pekee ya kujua ikiwa kuna uvujaji ni kupima 'heap' moja kwa moja. Chrome DevTools inakupa uwezo huo wa kuona.

Kutafuta Uvujaji kwa Kutumia Chrome DevTools

Unahitaji mfuatano unaoweza kurudiwa na dakika chache za umakini. Fungua programu yako kwenye Chrome, washa DevTools, na nenda kwenye tab ya Memory.

Rekodi msingi (baseline). Chagua Heap snapshot na ubonyeze Take snapshot. Hii inakamata kila kitu (object) kinachoishi sasa hivi kwenye JavaScript heap na kukuonyesha kumbukumbu ya kuanzia. Fanya hivi baada ya ukurasa kutulia katika hali yake ya kwanza ya kutulia (idle state), si wakati wa upakiaji wa awali, ili upime ukuaji unaosababishwa na vitendo vya mtumiaji pekee.

Chochea kitendo. Fanya mwingiliano wa UI ule ule unaohisi unasababisha uvujaji. Fungua na ufunge modal, badilisha chati tata, au badilisha njia (route) na urudi nyuma. Ukimaliza, rudisha programu katika hali yake ya awali ya kuonekana. Hatua hii ni muhimu sana. Unataka UI ionekane sawa na jinsi ilivyokuwa wakati wa msingi (baseline). Ikiwa heap imekua licha ya UI kuonekana tupu, una ushahidi thabiti wa uvujaji.

Lazimisha garbage collection. Bonyeza ikoni ya pipa la takataka kwenye tab ya Memory. Hii huchochea mzunguko kamili wa GC na kusafisha vitu vya muda ambavyo vilinasalia kwa uhalali hadi ukusanyaji ujao. Kinachobaki ni uvujaji halisi, vitu ambavyo vingepaswa kukusanywa lakini vilidumishwa hai kwa marejeo ya bahati mbaya.

Chukua snapshot ya pili. Bonyeza Take snapshot tena. Sasa unazo picha mbili za heap zilizopigwa chini ya hali sawa ya UI.

Linganisha matokeo. Badilisha muonekano kutoka Summary kwenda Comparison. Weka msingi (baseline) kwenye snapshot yako ya kwanza na snapshot inayolinganishwa kwenye ya pili. Muonekano wa Comparison unaorodhesha kila kundi la kitu (object category) na kukuonyesha delta, mabadiliko halisi katika idadi ya vitu kati ya picha hizo mbili.

Panga kwa Delta. Angalia makundi yaliyoongezeka kwa kiasi kikubwa. Lenga hasa kwenye Detached HTMLElement na React fiber nodes. HTMLElement iliyojitenga ni node ya DOM ambayo haijaunganishwa tena kwenye mti wa hati (document tree) unaofanya kazi, lakini rejea fulani ya JavaScript bado inaikamata. Hizi ni ushahidi wa wazi. Hazipaswi kuwepo baada ya kufunga modal au kutoa component (unmount).

Kusoma Retaining Path

When you select a detached element in the snapshot, Chrome shows the retaining path in the bottom panel. This path is a chain of references from the root down to the selected object. Follow it carefully. You will often find an event listener, an IntersectionObserver, a setInterval ID, or a closure pointing to a specific line in your component.

Look for names you recognize. If you see a listener attached to window with a function name from your codebase, you have found the anchor. The object holding that listener is keeping your entire component alive. Sometimes the chain runs through a third-party library. In those cases, check whether the library expects an explicit teardown call that you forgot to invoke in a cleanup function.

Fixing the Root Causes

Once you identify the retaining path, the fix is usually mechanical but requires discipline across the team.

Use cleanup functions. Always return a cleanup function in useEffect when you add listeners to the window or document. If your effect subscribes to a resize event, remove that subscription before the component unmounts. The cleanup runs when React tears down the component, giving you a guaranteed hook to sever external connections.

Stabilize references. Wrap your handlers in useCallback. This ensures you pass the exact same function reference to removeEventListener that you originally passed to addEventListener. If you register an inline function like window.addEventListener('resize', () => { ... }) and later try to remove it with another inline function, the references will not match. The listener stays attached. The closure inside it keeps your component state alive. useCallback with a stable dependency array prevents this identity mismatch.

Sever global links. Remember that the browser's event target objects, like window and document, live for the lifetime of the page. Any reference from them into your component acts as a global anchor. Removing the listener breaks that anchor and lets the V8 engine sweep away the component state and DOM nodes during the next garbage collection cycle.

Consider a modal that tracks window width. Without cleanup, each time the user opens the modal, a new listener attaches. The old ones never detach because the component instances they belong to are gone, yet the functions themselves were anonymous and lost. Using a named handler wrapped in useCallback, plus a cleanup function that calls removeEventListener, closes the loop cleanly.

A Real Takeaway

Memory leaks in React rarely announce themselves with a clear error message. They announce themselves with a tab that grows heavier the longer it stays open. Do not waste time speculating about which hook is guilty. Open the Memory tab, force garbage collection, and compare snapshots. Let the heap profiler show you the exact retaining path. Then write the cleanup function, stabilize the callback reference, and cut the global link. Your users will not notice the fix directly, but they will notice that the dashboard still runs smoothly at the end of the day.