Mundarija (28)
- Bu darsda
- 1. Nega bu kerak?
- 2. Sizishni qanday aniqlash: uch qadamli usul
- 3. Unutilgan tinglovchi
- 3.1 Sizishni qayta yaratamiz
- 3.2 Nega GC o'chirmadi
- 3.3 Node ogohlantiradi
- 4. To'xtatilmagan taymer
- 5. Closure ushlab qolishi
- 5.1 Kutilmagan sizish
- 5.2 Bitta funksiyaning closure'lari muhitni bo'lishadi
- 6. Detached DOM
- 6.1 Sahifadan olingan, lekin tirik
- 6.2 DevTools'da topish: heap snapshot
- 7. Chegarasiz kesh
- 8. Hujumchi nigohi
- 9. Ko'p uchraydigan xatolar
- 9.1 "Xotira o'sdi — demak sizish"
- 9.2 Anonim funksiyani olib tashlashga urinish
- 9.3 Ogohlantirishni o'chirish
- 9.4 Konsolda qoldirilgan obyektlar
- 10. Mashqlar
- 1-mashq (oson): Sizishni toping
- 2-mashq (o'rta): Sizishni o'lchab isbotlang
- 3-mashq (qiyin): Vazifalar qadami — sizish auditi
- 11. Real ishda
- Xulosa
- Manbalar
Xotira sizishlari va ularni topish: listener, taymer, closure, detached DOM va kesh
Qisqacha: Xotira sizishi (memory leak) — dasturga endi kerak bo'lmagan, lekin hali yetib boriladigan qiymat. GC uni o'chira olmaydi: kimdir unga havolani ushlab turibdi. Besh asosiy manba: olib tashlanmagan tinglovchi, to'xtatilmagan taymer, katta qiymatni ushlab qolgan closure, sahifadan olingan, lekin massivda qolgan DOM element (detached DOM) va chegarasiz kesh. Topish usuli: amalni bir necha marta takrorlab, xotira qaytmasligini o'lchash va heap snapshot'larni taqqoslab, "kim ushlab turibdi"ni ko'rish.
Bu darsda
- Xotira sizishining besh asosiy manbasini taniy olasiz va har birini tuzata olasiz.
- Sizishni Node'da
--expose-gcvaprocess.memoryUsage()bilan qayta yaratib, o'lchay olasiz. - Closure nega "ishlatmagan" o'zgaruvchisini ham ushlab qolishi mumkinligini tushuntira olasiz.
- Chrome DevTools Memory panelida heap snapshot olib, ikkitasini taqqoslay olasiz va ushlab turgan havolani (retainer) topa olasiz.
Oldin bilishingiz kerak: Xotira boshqaruvi va garbage collection, Listener opsiyalari: once, passive, signal, Closure tuzoqlari, Memoization.
1. Nega bu kerak?
Bu dars uchun kursda eng ko'p va'da yig'ilgan — oltita:
- unutilgan tinglovchi —
addEventListenerasoslari, Event delegation va Listener opsiyalari darslarida; - katta closure'lar — Closure tuzoqlari darsida;
- cheksiz o'sadigan kesh —
WeakMapva Memoization darslarida.
Hammasi bir savolga olib keladi: "sizishni qanday topaman?"
Oldingi darsda qoidani o'rgandik: GC faqat yetib bo'lmaydigan narsani o'chiradi. Sizish — shu qoidaning teskari tomoni. Siz obyektni "tashladim" deb o'ylaysiz, lekin dasturning qaysidir burchagida unga havola qolgan. GC uchun u tirik.
«Bahor»dan o'xshatish. Mehmon ketdi, lekin stol ustida "band" lavhasi qolib ketdi. Ofitsiant zalni aylanib chiqqanda bu stolga tegmaydi — "band-ku". Har kuni bir-ikkita shunday stol qoladi. Bir oydan keyin zal yarim bo'sh, lekin yangi mehmonga joy yo'q.
Sizish xato xabari bermaydi. Sayt birinchi daqiqada yaxshi ishlaydi. Soatlab ochiq turadigan sahifalarda — Gmail, Telegram Web, admin panel — u asta-sekin og'irlashadi va oxiri qotadi. Serverda esa bir necha kundan keyin JavaScript heap out of memory bilan qulaydi.
2. Sizishni qanday aniqlash: uch qadamli usul
Har qanday sizishni tekshirish bir xil:
- Boshlang'ich holat. GC'ni ishga tushirib, xotirani o'lchang.
- Amalni takrorlang. Shubhali amalni (oyna ochib-yopish, sahifa almashtirish) ko'p marta bajaring — oxirida dastur boshlang'ich holatga qaytsin.
- Yana GC va o'lchov. Xotira boshlang'ichga qaytsa — sizish yo'q. Har takrorda o'sib borsa — sizish bor.
Muhim nuqta — amaldan keyin dastur mantiqan boshlang'ich holatga qaytadi. Oyna ochildi va yopildi — ekranda hech narsa qolmadi. Demak, xotira ham qaytishi kerak.
flowchart LR
A["gc() va o'lchov"] --> B["Amalni N marta takrorlash"]
B --> C["gc() va o'lchov"]
C --> D{"Xotira qaytdimi?"}
D -- "ha" --> E["Sizish yo'q"]
D -- "har safar o'sadi" --> F["Sizish: kim ushlab turibdi?"]Endi besh manbani birma-bir ko'ramiz va har birini o'lchaymiz.
3. Unutilgan tinglovchi
3.1 Sizishni qayta yaratamiz
Node'da ham brauzerdagi kabi EventTarget bor — addEventListener va removeEventListener aynan bir xil ishlaydi. Shuning uchun sizishni Node'da, aniq raqamlar bilan qayta yarata olamiz. Vaziyat: buyurtma oynasi ochiladi va oshxonadan "tayyor" xabarini tinglaydi. Oyna yopiladi — lekin tinglovchi qoladi:
// node --expose-gc sizish.mjs (tuzatilgani: ... tuzat)
const tuzat = process.argv[2] === "tuzat";
const oshxona = new EventTarget();
function mb() {
gc(); // avval axlatni yig'amiz — faqat tirik xotira qolsin
return (process.memoryUsage().heapUsed / 2 ** 20).toFixed(1);
}
function oynaniOch() {
const tarix = new Array(10_000).fill("buyurtma"); // ~80 KB
const tinglovchi = () => console.log(tarix.length);
oshxona.addEventListener("tayyor", tinglovchi);
return function oynaniYop() {
if (tuzat) {
oshxona.removeEventListener("tayyor", tinglovchi);
}
};
}
console.log("boshida:", mb(), "MB");
for (let i = 1; i <= 1000; i++) {
const oynaniYop = oynaniOch();
oynaniYop();
if (i % 250 === 0) {
console.log(`${i} marta:`, mb(), "MB");
}
}Bu kod ikki rejimda ishlaydi: tuzat so'zisiz — sizadi, tuzat bilan — oynaniYop tinglovchini olib tashlaydi. Har rejimni 5 marta ishga tushirdik — natijalar har safar aynan bir xil chiqdi:
- tinglovchi qoldi
- removeEventListener
| Oyna ochildi va yopildi | tinglovchi qoldi | removeEventListener |
|---|---|---|
| 0 | 3,9 | |
| 250 | 23,4 | |
| 500 | 42,5 | |
| 750 | 61,7 | |
| 1 000 | 80,9 | |
| 0 | 3,9 | |
| 250 | 4,3 | |
| 500 | 4,4 | |
| 750 | 4,5 | |
| 1 000 | 4,5 |
Manba: O'lchov: Node 24.21, 12th Gen Intel Core i5-12500H, Windows 11, 2026-10-05; --expose-gc, har rejim 5 marta — natijalar bir xil
Nimaga qarang. Ekranda har safar hech narsa qolmaydi — oyna yopildi. Lekin sizadigan rejimda har 250 oynadan keyin xotira taxminan 19 MB o'sdi. 1000 oynadan keyin — 77 MB ortiqcha. Tuzatilgan rejimda chiziq tekis.
3.2 Nega GC o'chirmadi
Havolalar zanjirini kuzating: oshxona — global, ya'ni ildiz. Uning tinglovchilar ro'yxatida tinglovchi funksiyasi. Funksiya closure orqali tarix massivini ushlab turibdi (Closure). Zanjir uzilmagan — massiv tirik:
flowchart LR
R["oshxona (global)"] --> L["tinglovchilar ro'yxati"]
L --> F["tinglovchi funksiyasi"]
F --> T["tarix: 80 KB massiv"]removeEventListener ikkinchi o'qni kesadi. Shundan keyin funksiya va massivga yo'l qolmaydi.
Zanjir haqiqatan tirikligini xotirasiz ham ko'rish mumkin. Ikki oyna ochilib-yopiladi, keyin oshxona "tayyor" xabarini yuboradi. Yopilgan oynalar javob beradimi? Qadamlarda kuzating:
Oshxonada bu — mehmon ketgan, lekin ofitsiant hali ham o'sha stolga "buyurtmangiz tayyor" deb choy olib boryapti.
3.3 Node ogohlantiradi
Sizadigan rejimda chiqishning oxirida bunday qator ham bor (Node 24.21, haqiqiy matn):
(node:11284) MaxListenersExceededWarning: Possible EventTarget memory leak detected. 11 tayyor listeners added to EventTarget. MaxListeners is 10. Use events.setMaxListeners() to increase limitTarjimasi: "Ehtimoliy EventTarget xotira sizishi aniqlandi. EventTarget'ga 11 ta tayyor tinglovchisi qo'shildi. Chegara — 10". Node bitta hodisaga 10 dan ortiq tinglovchi qo'shilsa, shubhalanadi. Bu xato emas, faqat ogohlantirish — dastur davom etadi. Lekin bu xabarni ko'rsangiz, birinchi navbatda sizishni qidiring. "Chegarani oshiraman" deb setMaxListeners bilan ovozini o'chirish — kasallikni emas, haroratni davolash. Brauzer bunday ogohlantirmaydi — u yerda faqat o'lchov yordam beradi.
Tuzatish usullari (eng qulayidan):
{ signal }— Listener opsiyalari darsidagidek: bittaAbortControllerbilan bir nechta tinglovchini birdaniga olib tashlash;removeEventListener— aynan o'sha funksiya bilan (anonim funksiyani olib tashlab bo'lmaydi);{ once: true }— bir marta kerak bo'lsa;- Event delegation — har elementga emas, bitta ota-elementga bitta tinglovchi.
Tekshirib ko'ring:
tugma.addEventListener("click", () => saqla())qilib, keyintugma.removeEventListener("click", () => saqla())yozildi. Tinglovchi olib tashlanadimi?
Javob
Yo'q. Ikkita () => saqla() — ikki har xil funksiya obyekti, ular bir xil ko'rinsa ham. removeEventListener aynan qo'shilgan funksiyani qidiradi, topolmaydi va jim o'tib ketadi. Tuzatish: funksiyani o'zgaruvchiga saqlang yoki signal ishlating.
4. To'xtatilmagan taymer
setInterval (Taymerlar) siz to'xtatmaguncha ishlaydi. U ham ildiz: host muhit taymerlar ro'yxatini ushlab turadi, ro'yxat — callback funksiyani, funksiya esa closure orqali uning hamma o'zgaruvchilarini.
To'g'ri usul — taymerni har doim to'xtatish yo'lini o'ylab qo'yish:
const yangilanishlar = [];
const taymer = setInterval(() => {
yangilanishlar.push({ vaqt: "12:00", holat: "tayyorlanmoqda" });
}, 10);
setTimeout(() => {
clearInterval(taymer); // sahifa yopilganda shunday qiling
const bor = yangilanishlar.length > 0;
console.log("to'xtatildi:", bor); // to'xtatildi: true
}, 100);clearInterval bo'lmaganda nima bo'lardi? yangilanishlar massivi har 10 ms da o'sardi — cheksiz. Node'da yana bir belgi bor: faol taymer jarayonni yopilishga qo'ymaydi. Bitta setInterval(() => {}, 1000) dan iborat fayl hech qachon o'zi tugamaydi — biz uni 4 soniyadan keyin majburan to'xtatishga majbur bo'ldik. "Dastur ishini tugatdi, lekin terminal qaytmayapti" — ko'pincha unutilgan taymer.
Brauzerda klassik holat: sahifaning bir qismi (masalan, "Ish vaqti" paneli) har soniyada vaqtni yangilaydi. Foydalanuvchi boshqa bo'limga o'tdi, panel DOM'dan olib tashlandi — lekin setInterval ishlashda davom etadi va olib tashlangan panelni ushlab turadi.
5. Closure ushlab qolishi
5.1 Kutilmagan sizish
Bu eng ayyor sizish. Kodga qarang:
// node --expose-gc yopiq.mjs (tuzatilgani: ... tuzat)
function tugmaSizadi() {
const hisobot = new Array(100_000).fill(0); // ~800 KB
const sanash = () => hisobot.length; // hisobot'ni ushlaydi
sanash();
return () => "yuklandi"; // hisobot'ni ishlatmaydi!
}
function tugmaToza() {
const hisobot = new Array(100_000).fill(0);
const soni = hisobot.length; // closure'siz hisobladik
return () => `yuklandi: ${soni} qator`;
}
const yasovchi =
process.argv[2] === "tuzat" ? tugmaToza : tugmaSizadi;
const tugmalar = [];
for (let i = 0; i < 100; i++) {
tugmalar.push(yasovchi());
}
gc();
const mb = process.memoryUsage().heapUsed / 2 ** 20;
console.log("tirik xotira:", mb.toFixed(1), "MB");tugmaSizadi qaytaradigan funksiya hisobot ga umuman tegmaydi. Mantiqan, funksiya tugagach massiv axlat bo'lishi kerak. O'lchaymiz (Node 24.21, har rejim 5 marta, natija bir xil):
node --expose-gc yopiq.mjs → tirik xotira: 80.2 MB
node --expose-gc yopiq.mjs tuzat → tirik xotira: 3.9 MB100 ta massivning hammasi tirik qoldi! Nega?
5.2 Bitta funksiyaning closure'lari muhitni bo'lishadi
V8 bitta funksiya ichida yaratilgan closure'lar uchun bitta umumiy muhit (context) yaratadi — closure'lar ushlab turadigan o'zgaruvchilar "qutisi". Qaysi o'zgaruvchilar unga tushishini dvigatel tahlil paytida, kod ishlashidan oldin hal qiladi (JS dvigateli ichida): "ichki funksiyalardan birontasi hisobot ni ishlatadimi? Ha — sanash. Demak, hisobot muhitga tushadi". Qaytgan () => "yuklandi" ham shu muhitga havola qiladi — u hisobot ni ishlatmasa ham.
flowchart LR
T["tugmalar massivi"] --> Q["() => 'yuklandi'"]
Q --> M["umumiy muhit"]
S["sanash (tugadi)"] -.-> M
M --> H["hisobot: 800 KB"]Oshxonadagi o'xshashi. Rustam aka ikki shogirdiga bitta umumiy javon beradi. Birinchi shogirdga javondagi katta qozon kerak, ikkinchisiga — faqat tuz. Birinchisi ishini tugatib ketdi. Lekin ikkinchisi hali ishlayapti. U javonni ishlatayotgan ekan, javon ham, undagi qozon ham joyida turadi — qozonga endi hech kim tegmasa ham. Javon — umumiy muhit, qozon — hisobot, ikkinchi shogird — qaytgan () => "yuklandi".
tugmaToza da hisobot ni hech qanday ichki funksiya ishlatmaydi. U muhitga tushmaydi, oddiy lokal o'zgaruvchi bo'lib qoladi va funksiya tugashi bilan axlatga aylanadi.
Bu V8 ning ma'lum xatti-harakati: muhitni har closure uchun alohida qilish xotirani ham, tezlikni ham yeyardi. Siz uchun qoida: uzoq yashaydigan closure (tinglovchi, taymer callback'i, saqlanadigan funksiya) yaratadigan funksiyada katta qiymatni boshqa ichki funksiyaga ham bermang. Kerakli kichik qiymatni (soni) oldindan hisoblab oling.
Tekshirib ko'ring:
tugmaSizadidasanashqatorini olib tashlab,hisobot.lengthni to'g'ridan-to'g'ri ishlatsak (const soni = hisobot.length;), sizish qoladimi?
Javob
Yo'q. Endi hisobot ni hech qaysi ichki funksiya ishlatmaydi — u muhitga tushmaydi. Aynan shu tugmaToza — 3,9 MB.
6. Detached DOM
6.1 Sahifadan olingan, lekin tirik
Detached DOM ("uzilgan" DOM) — sahifadan olib tashlangan, lekin JavaScript'da havolasi qolgan element. U ekranda yo'q, lekin xotirada butun ichki daraxti bilan yashaydi. Quyidagi misolda bron oynasi 20 marta ochilib-yopiladi, lekin har oyna "tarix" massivida qoladi:
<button type="button" id="och">Yana 20 marta ochib-yopish</button>
<p id="holat"></p>
<script>
const ochilganlar = []; // ❌ yopilgan oynalarni ham saqlaydi
function oynaniOch() {
const oyna = document.createElement("div");
oyna.className = "oyna";
oyna.innerHTML =
"<p>Bron qabul qilindi</p><button>Yopish</button>";
document.body.append(oyna);
ochilganlar.push(oyna);
return oyna;
}
function yigirmaMarta() {
for (let i = 0; i < 20; i++) {
oynaniOch().remove(); // ochildi va darhol yopildi
}
const sahifada = document.querySelectorAll(".oyna").length;
document.getElementById("holat").textContent =
`Sahifada: ${sahifada}, massivda: ${ochilganlar.length}`;
console.log("sahifada:", sahifada);
console.log("massivda:", ochilganlar.length);
console.log("sahifaga ulanganmi:", ochilganlar[0].isConnected);
}
yigirmaMarta();
const tugma = document.getElementById("och");
tugma.addEventListener("click", yigirmaMarta);
</script>Konsolda:
sahifada: 0
massivda: 20
sahifaga ulanganmi: falseisConnected — element hozir sahifa daraxtiga ulanganmi (true/false). Sahifada birorta oyna yo'q, massivda esa 20 ta element — ularning har biri ichidagi <p> va <button> bilan. Tugmani bossangiz, son 40, 60 … bo'lib o'sadi.
Brauzerda ham removeEventListener unutilsa, xuddi shu holat ko'pincha yuzaga keladi: tinglovchi funksiyasi elementni closure'da ushlaydi. Tuzatish — elementni olib tashlaganda unga bo'lgan havolalarni ham tozalash: massivdan chiqarish (ochilganlar.length = 0 yoki splice), yoki element faqat ma'lumot uchun kerak bo'lsa — WeakMap/WeakRef.
6.2 DevTools'da topish: heap snapshot
Haqiqiy sahifada console.log qo'yib qidirmaysiz — buning uchun Chrome DevTools'ning Memory paneli bor. Heap snapshot — heap'dagi hamma obyekt va ular orasidagi havolalarning "surati". Bu oldingi darsdagi graf — faqat haqiqiy.
Tartib:
- Sahifani Live Server bilan oching, DevTools → Memory paneli → Heap snapshot → Take snapshot. Bu — 1-surat.
- Shubhali amalni bir necha marta bajaring (bizda — "Yana 20 marta" tugmasi).
- Yana Take snapshot — 2-surat.
- 2-suratni tanlab, yuqoridagi ro'yxatdan Comparison ni tanlang (1-surat bilan taqqoslash).
# New(yangi paydo bo'lgan),# Deleted(o'chirilgan),# Delta(farq) ustunlariga qarang. - Class filter maydoniga
Detacheddeb yozing.
Bizning sahifa uchun Chrome 154 da natija: Detached <div class="oyna"> guruhida 20 ta obyekt (biz buni avtomatik, DevTools protokoli orqali olingan snapshot bilan tekshirdik; ochilganlar massivisiz variantda — 0 ta).
- Qatorni oching va bitta obyektni tanlang. Pastdagi Retainers ("ushlab turuvchilar") bo'limi sizishning javobini beradi: bu obyektga kim havola qilyapti. Zanjirda
ochilganlarmassivini va uning ichidagi indeksni ko'rasiz.
Retainers — sizish qidirishdagi eng muhim joy. "Nima ko'p?" savoliga Comparison javob beradi, "nega o'chmayapti?" savoliga — Retainers. DevTools Memory panelining boshqa imkoniyatlarini DevTools: Performance, Memory va Network darsida to'liq o'rganamiz.
Maslahat: Snapshot olishdan oldin DevTools avtomatik GC qiladi — shuning uchun suratda faqat tirik obyektlar bo'ladi. Konsolga
console.log(element)qilib qoldirgan obyektlaringiz ham tirik qoladi: DevTools konsoli ularni ushlab turadi. O'lchashdan oldin konsolni tozalang.
7. Chegarasiz kesh
Memoization darsida ogohlantirgan edik: Map li kesh har yangi kalitni abadiy saqlaydi. Endi o'lchaymiz. Har so'rov — yangi kalit (masalan, foydalanuvchi kiritgan matn):
// node --expose-gc kesh.mjs (cheklangani: ... lru)
const lru = process.argv[2] === "lru";
const CHEGARA = 1000;
const kesh = new Map();
function narxniHisobla(sorov) {
if (kesh.has(sorov)) return kesh.get(sorov);
const javob = { sorov, jami: sorov.length * 1000 };
javob.izoh = "x".repeat(200);
kesh.set(sorov, javob);
if (lru && kesh.size > CHEGARA) {
kesh.delete(kesh.keys().next().value); // eng eskisi chiqadi
}
return javob;
}
function mb() {
gc();
return (process.memoryUsage().heapUsed / 2 ** 20).toFixed(1);
}
console.log("boshida:", mb(), "MB");
for (let i = 1; i <= 200_000; i++) {
narxniHisobla(`buyurtma-${i}`); // har so'rov — yangi kalit
if (i % 50_000 === 0) {
console.log(`${i} so'rov:`, mb(), "MB");
}
}Natija (Node 24.21, har rejim 3 marta — bir xil):
| So'rovlar | Chegarasiz Map |
LRU, 1000 yozuv |
|---|---|---|
| 0 | 3,9 MB | 3,9 MB |
| 50 000 | 22,8 MB | 4,6 MB |
| 100 000 | 41,3 MB | 4,6 MB |
| 200 000 | 78,4 MB | 4,6 MB |
Chegarasiz kesh — to'g'ri chiziq bo'ylab o'sish. Server kun bo'yi millionlab so'rov oladi — oxiri qulash. LRU bilan (eng uzoq ishlatilmagan yozuv chiqariladi — Memoization darsidagidek) xotira 1000 yozuvda to'xtadi. Kalitlar obyekt bo'lsa — WeakMap: obyekt o'lsa, yozuv ham o'zi ketadi.
8. Hujumchi nigohi
Chegarasiz kesh — nafaqat sizish, balki xavfsizlik teshigi. Tasavvur qiling: server foydalanuvchi qidiruv so'zini kalit qilib natijani keshlaydi. Hujumchi skript bilan millionlab har xil so'z yuboradi: a1, a2, a3 … Har biri yangi yozuv. Bir necha daqiqada server xotirasi tugaydi va u heap out of memory bilan qulaydi — hamma foydalanuvchi uchun. Bu xizmatni rad etish (DoS, Denial of Service) hujumining bir turi.
Himoya:
- har keshga chegara (LRU) va yashash muddati (TTL, Time To Live) — yozuv, masalan, 10 daqiqadan keyin o'zi o'chadi;
- tashqaridan keladigan kalit uzunligini cheklash (10 000 belgili "so'z" kalit bo'lmasin);
- serverda so'rovlar sonini cheklash (rate limiting: masalan, «Bahor» mashq API'si daqiqasiga 120 tadan ortiq so'rovni qabul qilmaydi) — backend qismida.
Xuddi shu mantiq tinglovchilarga ham tegishli: foydalanuvchi bir tugmani ming marta bossa, ming tinglovchi qo'shiladigan kod — o'z-o'ziga DoS.
9. Ko'p uchraydigan xatolar
9.1 "Xotira o'sdi — demak sizish"
Xotira GC'dan oldin o'sib turishi — normal (oldingi darsdagi "null dan keyin: 58 MB"). Tuzatish: har o'lchovdan oldin GC (gc() yoki snapshot olish) va amalni ko'p marta takrorlash. Faqat "har takrorda o'sib boradi" — sizish.
9.2 Anonim funksiyani olib tashlashga urinish
removeEventListener("click", () => …) — hech narsani olib tashlamaydi, xato ham bermaydi. Tuzatish: funksiyani nom bilan saqlang yoki signal ishlating.
9.3 Ogohlantirishni o'chirish
MaxListenersExceededWarning ni setMaxListeners(0) bilan "tuzatish". Tuzatish: avval sababni toping. Chegarani faqat haqiqatan ko'p tinglovchi kerak bo'lgan, o'lchangan holatda oshiring.
9.4 Konsolda qoldirilgan obyektlar
DevTools konsolida chiqarilgan obyektlar tirik qoladi va snapshot'ni "ifloslantiradi". Tuzatish: o'lchashdan oldin console.log larni olib tashlang va konsolni tozalang.
10. Mashqlar
1-mashq (oson): Sizishni toping
Quyidagi kodda qaysi qatorda sizish bor va nima ushlab qolinadi?
function profilniOch(foydalanuvchi) {
const panel = document.createElement("section");
panel.textContent = foydalanuvchi.ism;
document.body.append(panel);
window.addEventListener("resize", () => {
panel.style.width = `${window.innerWidth / 2}px`;
});
return () => panel.remove(); // yopish
}Yechim
window.addEventListener("resize", …) — tinglovchi hech qachon olib tashlanmaydi. window — ildiz, u tinglovchini, tinglovchi esa closure orqali panel ni ushlaydi. Panel remove qilingach ham detached DOM bo'lib tiriklayin qoladi, va har resize da hali ishlaydi. Har ochilish — yana bitta panel va tinglovchi.
Tuzatish — signal bilan:
function profilniOch(foydalanuvchi) {
const boshqaruv = new AbortController();
const panel = document.createElement("section");
panel.textContent = foydalanuvchi.ism;
document.body.append(panel);
window.addEventListener("resize", () => {
panel.style.width = `${window.innerWidth / 2}px`;
}, { signal: boshqaruv.signal });
return () => {
boshqaruv.abort(); // tinglovchi olib tashlandi
panel.remove();
};
}2-mashq (o'rta): Sizishni o'lchab isbotlang
sizish.mjs ni ko'chiring. Uni o'zgartiring: removeEventListener o'rniga { once: true } ishlating — tinglovchi bir marta ishlab, o'zi olib tashlansin. Oynani yopishdan oldin oshxona.dispatchEvent(new Event("tayyor")) bilan bitta "tayyor" xabarini yuboring. Xotira tekis qoladimi? Ishora: dispatchEvent hamma tinglovchilarni chaqiradi (Event obyekti).
Yechim
// node --expose-gc once.mjs
const oshxona = new EventTarget();
function mb() {
gc();
return (process.memoryUsage().heapUsed / 2 ** 20).toFixed(1);
}
function oynaniOch() {
const tarix = new Array(10_000).fill("buyurtma");
oshxona.addEventListener(
"tayyor",
() => tarix.length, // konsolni to'ldirmaslik uchun log yo'q
{ once: true },
);
}
console.log("boshida:", mb(), "MB");
for (let i = 1; i <= 1000; i++) {
oynaniOch();
oshxona.dispatchEvent(new Event("tayyor")); // tinglovchi o'chdi
}
console.log("1000 marta:", mb(), "MB");Xotira tekis qoladi: bizda (Node 24.21, 3 marta) boshida 3,9 MB, 1000 martadan keyin 4,4 MB: har tinglovchi bir marta ishladi va o'zini olib tashladi. Lekin diqqat: agar xabar hech qachon kelmasa, once tinglovchisi abadiy qoladi — once sizishdan himoya qilmaydi, faqat "bir martalik" vazifani soddalashtiradi. Oyna yopilishi xabar kelmasidan oldin bo'lishi mumkin bo'lsa — signal kerak.
3-mashq (qiyin): Vazifalar qadami — sizish auditi
vazifalar v3.1 kodini o'zgartirmaymiz — uni sizish nuqtai nazaridan tekshiramiz. assets/js/ dagi fayllarni oching va uch savolga javob bering:
asosiy.jsdaaddEventListenerlar qayerda chaqiriladi — harrender()da yoki bir marta?render.jsdagielonQiltaymeri sizishi mumkinmi?sinxron.jsdagitingla(ishlovchi)har chaqirilganda nima qiladi? Urender()ichiga ko'chirilsa nima bo'lardi?
Ishora: Event delegation va Taymerlar darslarini eslang.
Yechim
asosiy.jsda hammaaddEventListener— modulning yuqori darajasida, ya'ni bir marta. Ro'yxat qatorlari uchun alohida tinglovchi yo'q: bittaroyxat.addEventListener("click", …)delegation bilan ishlaydi.render()esareplaceChildrenbilan qatorlarni almashtiradi — eski qatorlarga tinglovchi bog'lanmagan, shuning uchun ular axlatga aylanadi.- Yo'q.
elonQilhar chaqiruvda avvalclearTimeout(elonTaymeri)qiladi — bir vaqtda faqat bitta taymer bor. Bu 11-qismdagi qadam edi. tinglahar chaqirilgandaBroadcastChannelga yangi tinglovchi qo'shadi. Hozir uasosiy.jsning oxirida bir marta chaqiriladi — muammo yo'q.render()ichiga ko'chirilsa, har render bitta tinglovchi qo'shardi: 100 render — boshqa tabdan kelgan bitta xabar uchun 100 marta qayta yuklash. Klassik tinglovchi sizishi.
Xulosa: kanon kod sizishdan xoli — sababi delegation va "tinglovchi bir marta, modul darajasida" qoidasi. Topilganlarni kurs/mashqlar/12/31-sizish/audit.md ga yozing:
git add 12/31-sizish/audit.md
git commit -m "12/31: vazifalar sizish auditi"11. Real ishda
- SPA (bir sahifali ilovalar). React, Vue kabi freymvorklarda komponent o'chirilganda tinglovchi va taymerlarni tozalash uchun maxsus joy bor (React'da
useEffectqaytaradigan "tozalash" funksiyasi). Uni unutish — eng ko'p uchraydigan sizish. 17-qismda ko'rasiz. - Node serverlari. Xotira grafigi doim yuqoriga qarab o'sib borsa — sizish. Serverda heap snapshot olish uchun Node'da
v8.writeHeapSnapshot()bor: uHeap.20261005.143600.7220.0.001.heapsnapshotkabi fayl yozadi, uni Chrome DevTools Memory paneliga yuklab, xuddi brauzerdagidek tahlil qilasiz. Batafsil — backend qismida. - Kutubxonalar.
lru-cachepaketi — chegarali kesh;AbortController— tinglovchi vafetchlarni birdaniga bekor qilish. - Intervyu: "JavaScript'da qanday xotira sizishlarini bilasiz?", "Detached DOM nima?", "Heap snapshot bilan sizishni qanday topasiz?" — middle darajadagi frontend intervyularida juda ko'p so'raladi.
Xulosa
- Xotira sizishi — kerak bo'lmagan, lekin hali yetib boriladigan qiymat; GC uni o'chira olmaydi.
- Besh manba: tinglovchi, taymer, closure muhiti, detached DOM, chegarasiz kesh. Har birida ildizdan obyektgacha uzilmagan havola zanjiri bor.
- Aniqlash: GC → amalni N marta takrorlash → GC → o'lchov; bizda 1000 oyna — 77 MB sizish,
removeEventListenerbilan — deyarli 0. - Bitta funksiyaning closure'lari muhitni bo'lishadi: ishlatilmagan katta o'zgaruvchi ham ushlab qolinishi mumkin.
- DevTools: ikki heap snapshot → Comparison →
Detachedfiltri → Retainers — kim ushlab turibdi. - Chegarasiz kesh — sizish ham, DoS teshigi ham; LRU va TTL bilan cheklang.
Keyingi dars: Performansni o'lchash va benchmarking — vaqtni to'g'ri o'lchashni, isitish va mediana nima uchun kerakligini va mikro-benchmark qanday aldashini o'rganamiz.
Manbalar
- Chrome DevTools hujjatlari: "Fix memory problems", "Record heap snapshots" — developer.chrome.com/docs/devtools/memory-problems
- MDN: "Memory management",
EventTarget.removeEventListener(),Node.isConnected— developer.mozilla.org - Node.js hujjatlari:
events.setMaxListeners(),v8.writeHeapSnapshot()— nodejs.org/api - V8 blogi: "Speeding up V8 heap snapshots" (2023-07-27) — v8.dev/blog
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!