Ripoti ya hitilafu (bug report) ilifika ambayo ilipinga kila silika ya utatuzi wa hitilafu (debugging). Watumiaji wa simu za Android zenye uwezo mdogo walisema programu ilitoweka tu. Sio wakati wa kuanza. Sio wakati wa kugusa au kusogeza skrini fulani. Takriban dakika ishirini baada ya kuanza, skrini iliganda na mchakato ukakufa. Logi zilikuwa safi kabisa. QA hawakuweza kuirejesha kwenye vifaa vyao vyenye uwezo mkubwa. Hakukuwa na hatua za kufuata. Baada ya saa tatu za uchambuzi wa kumbukumbu (memory profiling), picha hatimaye ilionekana wazi. Msikilizaji mmoja wa matukio (event listener) ulikuwa ndani ya React hook. Msikilizaji huo ulikuwa umefunga (closed over) seti kubwa ya data. Komponent iliondolewa (unmounted). Msikilizaji ulibaki. Seti ya data ilibaki kwenye kumbukumbu. Kwenye kifaa chenye RAM ya 2GB, mkusanyiko huo uliisha heap na mfumo wa uendeshaji ukaua programu. Hii haikuwa hitilafu ya sintaksia wala kasoro ya kimantiki. Ilikuwa hitilafu ya upeo (scope bug), na ilikuwa hatari sana.
Jinsi Closure Inavyokuwa Sababu ya Kuvuja kwa Kumbukumbu (Leak)
Mafunzo mengi hufundisha upeo (scope) kama fumbo la kitaaluma kuhusu wapi variable inaonekana. Katika uzalishaji (production), upeo ni mkataba kuhusu muda wa kuishi kwa kumbukumbu. Wakati kazi ya JavaScript inafunga (closes over) variable, injini huifanya variable hiyo iendelee kuishi mradi tu closure yenyewe inaweza kufikiwa. Katika React component, hii inamaanisha data yako hudumu muda mrefu baada ya mtumiaji kuondoka na UI node kuondolewa.
Fikiria hook inayosajili msikilizaji kwenye kitu cha window. Komponent inatengenezwa (renders), inaambatisha msikilizaji, na baadaye inaondolewa (unmounts). Ikiwa hatua ya usafishaji (cleanup phase) imepungua au imefanywa vibaya, msikilizaji unabaki. Kila mara mpya ya kuunganishwa (mount) huongeza nakala nyingine ya mzimu (ghost copy) ya data iliyofungwa kwenye RAM. Kwenye kituo cha kazi cha mwanatengenezaji chenye kumbukumbu nyingi, huenda usigundue ongezeko hilo. Kwenye simu ya bei nafuu inayotumia Android Go, dakika ishirini za matumizi ya kawaida zinatosha kuisha heap inayopatikana. Mfumo wa uendeshaji (OS) unakuja na kusitisha mchakato. Hakuna hitilafu ya kuandika kwenye logi. Mfumo unazima tu kila kitu.
Hii ndiyo sababu upeo ni usimamizi wa kumbukumbu. Mazingira ya kileksika (lexical environment) si mpaka wa kifalsafa. Ni grafu ya uhifadhi (retention graph). Kila variable unayoacha ndani ya closure ambayo haijakusanywa ni tofali katika ukuta ambao hatimaye utazifungia programu yako.
Njia Tatu Ambazo Upeo (Scope) Unaharibu Programu za Uzalishaji (Production Apps)
Matatizo ya upeo hayafanani yote. Baadhi yanachuja kumbukumbu polepole. Mengine yanapasuka papo hapo. Hapa kuna mifumo inayozima programu kwa uhakika.
Uchafuzi wa Upeo wa Kimataifa (Global Scope Pollution)
Miundo ya micro-frontend inaruhusu timu kutuma programu huru, lakini zote zinashiriki kitu kimoja cha window. Wakati programu moja inapoweka variable ya kimataifa kama window.config au inapoboresha utility inayoshirikiwa kwenye window, haikai peke yake. Programu ya timu nyingine inaweza kutegemea muundo tofauti wa hiyo hiyo global, au kuifuta wakati wa kuanza kwake (bootstrap). Matokeo yake ni mgongano wa vipengele (feature collision) unaoongezeka kadiri shirika lako linavyokua. Mwanatengenezaji katika sehemu moja ya msimbo (repository) hana habari kwamba njia yake ya mkato ni mabadiliko yanayovunja programu ya timu nyingine. Kadiri eneo la kazi linavyokua, hizi globals zinakuwa kama mabomu ya ardhini (landmines) yaliyozikwa kwenye udongo wa pamoja.
Kuvuja kwa Kumbukumbu Kutokana na Closure
Programu za ukurasa mmoja (Single page applications) zimeundwa kufanya kazi kwa saa nyingi. Uimara huo ndio sababu hasa closure zinazovuja zinakuwa sumu. Mtindo huu ni wa kawaida kwa namna inayodanganya: useEffect inasajili callback na global event bus, WebSocket handler, au DOM yenyewe. Ikiwa array ya utegemezi (dependency array) haijatulia au haikuwekwa, usafishaji hautalingana na usajili wa awali. Closure inakamata chochote kilicho katika mazingira yake ya kileksika (lexical scope), ambacho kinaweza kujumuisha array kubwa zilizochambuliwa (parsed arrays), JSON blobs zilizopatikana, au marejeleo ya miti ya DOM. Kila mabadiliko ya ukurasa (navigation) huongeza uzito zaidi. Mtumiaji hajui kwa nini tab ya kivinjari chake inatumia MB 800. Anajua tu kwamba programu inahisi nzito na hatimaye inakufa.
Hii ni hatari hasa wakati dependency arrays zinabadilika kila wakati wa render. Marejeleo mapya ya kazi (function reference) huzaliwa katika kila mzunguko, anasajiliwa na msikilizaji, na ya zamani haitolewi kamwe. Matokeo yake ni makumbusho ya closure zilizokufa, kila moja ikihifadhi data iliyozaliwa nayo.
Makosa ya TDZ katika Moduli za Kidinamiki (Dynamic Modules)
Temporal Dead Zone si tukio la nadharia tu la pembeni. Unapojaribu kutumia let au const kabla ya tamko (declaration) lake kutekelezwa, injini hutoa ReferenceError. Katika monorepos kubwa zenye circular dependencies na dynamic imports, mpangilio kamili wa utekelezaji mara nyingi huwa wa mafumbo (implicit). Module A ina-import Module B, ambayo ina-import chunk kwa njia ya kidinamiki inayotegemea tena Module A. Ikiwa tawi moja litagusa variable ambayo haijamaliza kuanzishwa (initializing), programu itafeli (crash) wakati wa kupakia. Makosa haya yanachanganya kwa sababu yanategemea muda (timing). Mabadiliko madogo katika maeneo ya mgawanyo ya bundler (bundler split points), ucheleweshaji wa mtandao wakati wa kupakia kodi, au mabadiliko katika uhifadhi wa chunk (chunk caching) yanaweza kubadilisha mpangilio kiasi cha kuchochea TDZ. Hitilafu hiyo haina utabiri, na stack trace mara nyingi huonyesha mstari wa kodi ambao hauna kosa lolote.
Mbinu za Kinga
Huwezi kutegemea stack traces kukuokoa kutoka kwa makosa ya scope. Unahitaji kuzuia na kugundua.
Anza na static analysis. Sanidi ESLint ili kusimamia mipaka madhubuti. Sheria kama no-implicit-globals na no-shadow hukamata makosa ya wazi. Shadowing ni hatari hasa kwa sababu inakudanganya ufikiri unabadilisha variable ya ndani (local variable) wakati kwa kweli unajenga closure juu ya variable ya nje, au unaunda nakala ya bahati mbaya. Sheria hizi hulazimisha nia ya wazi na kuondoa migongano ya kimya.
Chunguza (profile) kumbukumbu yako kwa nidhamu ile ile unayotumia kwenye unit tests. Fungua Chrome DevTools, chukua heap snapshot kwenye njia yako ya kuanzia (starting route), tembea kwenye programu yako kwa dakika tano, kisha chukua nyingine. Linganisha hizo mbili. Chuja kwa ajili ya "Closure" na utafute idadi zinazoongezeka bila kikomo. Tafuta node za DOM zilizojitenga (detached DOM nodes) ambazo bado zina event listeners. Ikiwa snapshot ya pili inaonyesha maelfu ya entries mpya za Closure wakati idadi ya watumiaji imebaki vilevile, basi una function zilizofungwa zinazoshikilia data zilizofungwa. Hilo ndilo tatizo lako la uvujaji wa kumbukumbu (leak).
Kikabili (Architecturally), acha kutumia object ya window ya kimataifa kwa ajili ya usanidi (configuration). Pitisha mipangilio kama props au kupitia typed context. Dependency injection si neno la kisasa la biashara hapa; ni utaratibu wa kuipa function kila kitu inachohitaji kupitia argument badala ya kuiacha ichungulie kwenye global scope. Matokeo yake ni kodi unayoweza kuijaribu bila browser shims, na module ambazo hazigongani wakati programu nyingi zinapowekwa (mount) ndani ya shell moja.
Mwishowe, heshimu hatua ya usafishaji (cleanup phase) bila huruma. Kila addEventListener inahitaji removeEventListener inayolingana ndani ya effect cleanup. Kwa kazi za asynchronous, tumia AbortController na upitishe signal yake kwenye fetch ili maombi yanayoendelea (in-flight requests) yakatishwe wakati component inapofia. Tabia hizi zinadhibiti moja kwa moja jinsi scope inavyoishi. Hizi si boilerplate. Hizi ni usimamizi wa kumbukumbu (memory management).
Hii Inamaanisha Nini kwa Timu Yako
Scope si mchezo wa ujanja wa kuwapa maswali waajiriwa wakati wa mahojiano. Katika uzalishaji (production), scope ni usimamizi wa kumbukumbu. Kila variable unayotangaza ni mfungwa anayeweza kutumika kama matege. Kila closure ni ahadi ambayo injini itatimiza. Unaposahau kuachilia listener, hauachi tu taa ikiwa imewaka. Unafunga uzito kwenye programu yako na kuituangusha baharini. Kwenye vifaa vyenye nguvu, programu itaogelea tu. Kwa watumiaji wa vifaa duni, itazama. Anza kuchukulia scope kama rasilimali yenye ukomo ilivyo. Watumiaji wako, na vipindi vyako vya debugging vya saa tatu, watakushukuru.
