IlmHamroh
JavaScript Full-stack/12-qism. JavaScript: ilg'or mavzular va kod sifati31/44-dars19 daqiqa
Mundarija (28)

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-gc va process.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:

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:

  1. Boshlang'ich holat. GC'ni ishga tushirib, xotirani o'lchang.
  2. Amalni takrorlang. Shubhali amalni (oyna ochib-yopish, sahifa almashtirish) ko'p marta bajaring — oxirida dastur boshlang'ich holatga qaytsin.
  3. 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:

js
// 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:

Oyna ochib-yopish: tirik xotira (gc() dan keyin)
heapUsed, MB
80,93,901 000Oyna ochildi va yopildi, martatinglovchi qoldi: 0 marta → 3,9 MBtinglovchi qoldi: 250 marta → 23,4 MBtinglovchi qoldi: 500 marta → 42,5 MBtinglovchi qoldi: 750 marta → 61,7 MBtinglovchi qoldi: 1 000 marta → 80,9 MBremoveEventListener: 0 marta → 3,9 MBremoveEventListener: 250 marta → 4,3 MBremoveEventListener: 500 marta → 4,4 MBremoveEventListener: 750 marta → 4,5 MBremoveEventListener: 1 000 marta → 4,5 MB
  • tinglovchi qoldi
  • removeEventListener
Oyna ochib-yopish: tirik xotira (gc() dan keyin)
Oyna ochildi va yopilditinglovchi qoldiremoveEventListener
03,9
25023,4
50042,5
75061,7
1 00080,9
03,9
2504,3
5004,4
7504,5
1 0004,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):

text
(node:11284) MaxListenersExceededWarning: Possible EventTarget memory leak detected. 11 tayyor listeners added to EventTarget. MaxListeners is 10. Use events.setMaxListeners() to increase limit

Tarjimasi: "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: bitta AbortController bilan 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, keyin tugma.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:

js
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:

js
// 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):

text
node --expose-gc yopiq.mjs        →  tirik xotira: 80.2 MB
node --expose-gc yopiq.mjs tuzat  →  tirik xotira: 3.9 MB

100 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: tugmaSizadi da sanash qatorini olib tashlab, hisobot.length ni 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:

html
<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:

text
sahifada: 0
massivda: 20
sahifaga ulanganmi: false

isConnected — 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:

  1. Sahifani Live Server bilan oching, DevTools → Memory paneli → Heap snapshot → Take snapshot. Bu — 1-surat.
  2. Shubhali amalni bir necha marta bajaring (bizda — "Yana 20 marta" tugmasi).
  3. Yana Take snapshot — 2-surat.
  4. 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.
  5. Class filter maydoniga Detached deb 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).

  1. Qatorni oching va bitta obyektni tanlang. Pastdagi Retainers ("ushlab turuvchilar") bo'limi sizishning javobini beradi: bu obyektga kim havola qilyapti. Zanjirda ochilganlar massivini 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):

js
// 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?

js
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:

js
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
js
// 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:

  1. asosiy.js da addEventListener lar qayerda chaqiriladi — har render() da yoki bir marta?
  2. render.js dagi elonQil taymeri sizishi mumkinmi?
  3. sinxron.js dagi tingla(ishlovchi) har chaqirilganda nima qiladi? U render() ichiga ko'chirilsa nima bo'lardi?

Ishora: Event delegation va Taymerlar darslarini eslang.

Yechim
  1. asosiy.js da hamma addEventListener — modulning yuqori darajasida, ya'ni bir marta. Ro'yxat qatorlari uchun alohida tinglovchi yo'q: bitta royxat.addEventListener("click", …) delegation bilan ishlaydi. render() esa replaceChildren bilan qatorlarni almashtiradi — eski qatorlarga tinglovchi bog'lanmagan, shuning uchun ular axlatga aylanadi.
  2. Yo'q. elonQil har chaqiruvda avval clearTimeout(elonTaymeri) qiladi — bir vaqtda faqat bitta taymer bor. Bu 11-qismdagi qadam edi.
  3. tingla har chaqirilganda BroadcastChannel ga yangi tinglovchi qo'shadi. Hozir u asosiy.js ning 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:

bash
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 useEffect qaytaradigan "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: u Heap.20261005.143600.7220.0.001.heapsnapshot kabi fayl yozadi, uni Chrome DevTools Memory paneliga yuklab, xuddi brauzerdagidek tahlil qilasiz. Batafsil — backend qismida.
  • Kutubxonalar. lru-cache paketi — chegarali kesh; AbortController — tinglovchi va fetch larni 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, removeEventListener bilan — deyarli 0.
  • Bitta funksiyaning closure'lari muhitni bo'lishadi: ishlatilmagan katta o'zgaruvchi ham ushlab qolinishi mumkin.
  • DevTools: ikki heap snapshot → Comparison → Detached filtri → 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
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Xotira sizishlari va ularni topish: listener, taymer, closure, detached DOM va kesh — IlmHamroh