Web programlama eğitimi kapsamında, tek sayfa uygulamalarında Detached DOM kaynaklı bellek sızıntılarını Chrome DevTools ve CDP ile ölçmeyi, framework yaşam döngülerinde kalıcı referansları kesmeyi inceliyoruz.
Web Programlama Eğitimi: Detached DOM ile Bellek Sızıntısı Avı
Web programlama eğitimi ile Detached DOM sızıntısını yeniden üretmek
Detached DOM, ekrandan kaldırılmış bir düğümün JavaScript heap içinde hala erişilebilir kalmasıdır. DOM düğümünün kendi içindeki listener döngüsü tek başına sızıntı yaratmaz; garbage collector erişilemeyen döngüleri toplar. Sızıntının oluşması için document üzerindeki bir listener, global cache, timer callback veya store gibi bir GC root'un düğüme ya da onu tutan closure'a erişmesi gerekir. Aşağıdaki örnekte document üzerindeki click listener'ı, kaldırılmış paneli closure ile tutar. Uygulamada 20 kez mount/unmount yaptıktan sonra Chrome DevTools > Memory > Heap snapshot ile snapshot alın, sağ üstteki çöp kutusu simgesiyle GC tetikleyin ve ikinci snapshot'ta Detached HTMLDivElement sayısını kontrol edin.
const retainedPanels = [];
function mountPanel() {
const panel = document.createElement('div');
panel.className = 'report-panel';
panel.innerHTML = '<button>Kapat</button>';
document.body.append(panel);
function onDocumentClick(event) {
if (!panel.contains(event.target)) {
console.log(panel.className); // closure panel'i canlı tutar
}
}
document.addEventListener('click', onDocumentClick);
retainedPanels.push({ panel, onDocumentClick });
return () => panel.remove(); // listener ve global referans kalır
}
Bu örnek, javascript eğitimi içeriklerinde sık görülen 'remove() çağrıldıysa bellek serbesttir' varsayımının neden hatalı olduğunu gösterir: DOM tree'den kopmak, JavaScript nesne grafiğinden kopmak değildir.Frontend development eğitimi için Chrome DevTools heap analizi
Ölçümü yalnızca Task Manager'daki process memory ile yapmayın; V8 heap, code space ve tarayıcı allocator davranışı nedeniyle bu sayı yönlendirici değildir. Chrome DevTools'ta önce uygulamayı kararlı başlangıç rotasına getirin, GC çalıştırın ve 'Baseline' heap snapshot alın. Ardından rota değişimini 20-50 kez tekrarlayın, GC çalıştırıp 'After navigation loop' snapshot alın. İkinci snapshot görünümünde 'Comparison' seçin, Constructor filtresine 'Detached' yazın ve pozitif 'Size Delta' değerlerini inceleyin. Bir Detached HTMLDivElement seçildiğinde 'Retainers' zincirinde document, EventListener, Closure veya bir Zustand/Redux store alanı görünüyorsa, nesneyi yaşatan gerçek kökü bulmuş olursunuz.
Tahmini değil tekrarlanabilir veri üretmek için Chrome DevTools Protocol üzerinden Allocation Sampling kullanın. Sampling, her allocation'ı kaydetmez; istatistiksel örnekleme yaptığı için düşük overhead ile hangi çağrı yığınının sürekli allocation yaptığını gösterir. Örneğin Chromium'u uzaktan hata ayıklama portuyla açıp Puppeteer üzerinden belirli akışı çalıştırabilirsiniz:
google-chrome --remote-debugging-port=9222
node leak-profile.mjs
// leak-profile.mjs
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserURL: 'http://127.0.0.1:9222'
});
const page = (await browser.pages())[0];
const cdp = await page.target().createCDPSession();
await cdp.send('HeapProfiler.startSampling', {
samplingInterval: 32768
});
for (let i = 0; i < 30; i++) {
await page.goto('http://localhost:5173/reports');
await page.click('[data-testid="open-filter"]');
await page.click('[data-testid="close-filter"]');
}
const { profile } = await cdp.send('HeapProfiler.stopSampling');
console.log(JSON.stringify(profile, null, 2));
await browser.disconnect();
Profilde mountPanel, addEventListener veya store'a kayıt yapan fonksiyonun selfSize değeri artıyorsa, heap snapshot'taki retainer zinciriyle aynı kod yolunu doğrulayın. Sampling sonucu tek başına 'leak' kanıtı değildir; GC sonrası karşılaştırmalı heap snapshot ile birleştirilmelidir.AbortController ile global listener ve timer referanslarını kesmek
Düzeltmenin hedefi DOM'u kaldırmak değil, onun dışındaki güçlü referansları atomik olarak kapatmaktır. addEventListener'ın signal seçeneği, aynı yaşam döngüsündeki birden çok listener'ı tek abort çağrısıyla siler. Timer için AbortSignal yerleşik destek sunmadığından timeout kimliğini ayrıca temizleyin. Bu değişiklikten önce ve sonra aynı 30 rota döngüsünü çalıştırın; 'Detached HTMLDivElement' count ve shallow size delta'nın sıfıra veya sabit küçük bir değere dönmesini bekleyin. Sadece listener'ı kaldırıp retainedPanels gibi debug cache'ini bırakmak, sorunu gizlemez.
function mountPanel() {
const controller = new AbortController();
const panel = document.createElement('div');
document.body.append(panel);
const timeoutId = setTimeout(() => {
panel.dataset.ready = 'true';
}, 1000);
document.addEventListener('click', (event) => {
if (!panel.contains(event.target)) panel.remove();
}, { signal: controller.signal });
return () => {
clearTimeout(timeoutId);
controller.abort();
panel.remove();
};
}
Önemli edge case: aynı AbortController'ı bir komponent instance'ından diğerine taşımayın. Bir kez abort edilmiş signal ile eklenen listener anında etkisiz olur; yeniden mount sonrası çalışan ama etkileşime tepki vermeyen panel hataları bu nedenle ortaya çıkar.Reactjs eğitimi, angular eğitimi ve vuejs eğitimi yaşam döngüsü farkları
reactjs eğitimi sırasında useEffect cleanup'ının yalnızca unmount'ta değil, dependency değişiminden hemen önce de çalıştığını hesaba katın. Aşağıdaki pattern, her reportId değişiminde eski document listener'ını kaldırır. React Strict Mode geliştirme ortamında effect'i mount-cleanup-mount dizisiyle sınayabilir; cleanup'ın idempotent olmaması burada production'da görünmeyen bir hata üretebilir.
useEffect(() => {
const controller = new AbortController();
const onKeyDown = (event) => {
if (event.key === 'Escape') closeReport(reportId);
};
document.addEventListener('keydown', onKeyDown, {
signal: controller.signal
});
return () => controller.abort();
}, [reportId, closeReport]);
angular eğitimi için Angular'ın DestroyRef ve @angular/core/rxjs-interop paketindeki takeUntilDestroyed operatörünü kullanın; subscribe sonucu elde edilen Subscription'ı unutmak yerine destroy sinyalini akışa bağlarsınız. vuejs eğitimi tarafında onUnmounted içinde listener temizliği veya watchEffect callback'inin onCleanup parametresi aynı amaca hizmet eder. Eski angularjs eğitimi projelerinde ise $scope.$on('$destroy', cleanup) kullanın; route cache kullanan uygulamalarda scope gerçekten destroy edilmeyebileceği için ui-router view cache ayarını ayrıca doğrulayın.
// Angular
private readonly destroyRef = inject(DestroyRef);
ngOnInit() {
this.socket.messages$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(message => this.render(message));
}
// Vue 3
onMounted(() => window.addEventListener('resize', resize));
onUnmounted(() => window.removeEventListener('resize', resize));
frontend framework karşılaştırması yaparken yalnızca render benchmark'ına bakmak bellek yaşam döngüsünü açıklamaz. React'te effect cleanup, Angular'da DestroyRef, Vue'da effect scope ve onUnmounted mekanizmalarını aynı test senaryosunda değerlendirin: 30 mount/unmount sonrası heap snapshot'ta component constructor sayısının başlangıç seviyesine dönmesi ölçüttür. react native eğitimi projelerinde DOM olmadığı için 'Detached DOM' aranmaz; ancak AppState, Dimensions veya NativeEventEmitter abonelikleri JavaScript closure'larını canlı tutabilir. Her aboneliğin remove() veya unsubscribe() çağrısını component cleanup'ına koymak gerekir.
Playwright ve CDP ile bellek regresyonunu CI'a taşımak
Manuel snapshot incelemesini release kontrolüne dönüştürmek için Playwright ile sabit bir kullanıcı akışını tekrar edin ve CDP Performance.getMetrics içindeki JSHeapUsedSize değerini kaydedin. Bu metrik GC zamanlamasına duyarlı olduğundan tek ölçüm yerine üç bağımsız koşunun medyanını alın. Ayrıca test Chromium'unu --js-flags=--expose-gc ile başlatıp HeapProfiler.collectGarbage çağrısıyla karşılaştırma öncesi GC isteyin. Önce sızıntılı sürümün örneğin 30 turda 18 MB büyüdüğünü, cleanup sonrası aynı senaryoda 2 MB eşik altında kaldığını kendi CI ortamınızda kaydedin; mutlak MB eşiğini farklı runner'lara kopyalamayın.
import { chromium } from '@playwright/test';
const browser = await chromium.launch({
args: ['--js-flags=--expose-gc']
});
const page = await browser.newPage();
const cdp = await page.context().newCDPSession(page);
await page.goto('http://localhost:4173');
await cdp.send('HeapProfiler.collectGarbage');
const before = await cdp.send('Performance.getMetrics');
for (let i = 0; i < 30; i++) {
await page.getByTestId('open-report').click();
await page.getByTestId('close-report').click();
}
await cdp.send('HeapProfiler.collectGarbage');
const after = await cdp.send('Performance.getMetrics');
const value = (metrics, name) => metrics.metrics.find(x => x.name === name).value;
const growthMb = (value(after, 'JSHeapUsedSize') - value(before, 'JSHeapUsedSize')) / 1024 / 1024;
if (growthMb > 2) throw new Error(`Heap growth: ${growthMb.toFixed(2)} MB`);
await browser.close();
Bu kontrol, farklı sayfaların normal cache büyümesi nedeniyle false positive verebilir. Bu yüzden testte ağ yanıtlarını sabitleyin, analytics scriptlerini kapatın, başlangıç rotasını her koşuda aynı hale getirin ve eşiği önce temiz build'lerin medyanı ile belirleyin.İlgili Eğitim
YTÜSEM İlgili Eğitim
Angular İle Web Uygulama Geliştirme (en yakın eşleşen) (Yıldız Teknik Üniversitesi SEM)
Sık Sorulan Sorular
Web programlama eğitimi içinde Detached DOM nasıl tespit edilir?
Chrome DevTools > Memory bölümünde GC sonrası iki Heap Snapshot alın ve ikinci snapshot'ı Comparison görünümünde açın. 'Detached' filtresiyle HTMLDivElement kayıtlarını bulun, ardından Retainers zincirinde document listener, closure, global array veya store referansı olup olmadığını inceleyin. Sadece allocation sayısına değil, GC sonrasındaki pozitif retained size delta'ya bakın.
Reactjs eğitimi için useEffect bellek sızıntısı nasıl engellenir?
Her effect dış kaynağı için cleanup döndürün: document listener için AbortController.abort(), timer için clearTimeout(), fetch için abort(), RxJS aboneliği için unsubscribe() kullanın. Dependency değişiminde cleanup'ın da çalıştığını unutmayın; handler fonksiyonunu effect içinde üretmek veya useCallback ile kararlı hale getirmek, yanlış listener kaldırma riskini azaltır.
Angular eğitimi projelerinde subscribe bellek sızıntısı nasıl ölçülür?
Komponenti 30 kez oluşturup yok eden Playwright senaryosundan sonra Chrome heap snapshot alın. Retainer zincirinde Subscriber, SafeSubscriber veya komponent instance'ı kalıyorsa subscription kapanmamıştır. Angular'da takeUntilDestroyed(inject(DestroyRef)) ile akışı destroy yaşam döngüsüne bağlayın ve aynı senaryoda retained component sayısının sabitlendiğini doğrulayın.
Frontend framework karşılaştırması yapılırken bellek testi nasıl adil yapılır?
React, Angular ve Vue uygulamalarında aynı veri seti, aynı 30 mount/unmount akışı, aynı Chromium sürümü ve kapatılmış üçüncü taraf scriptlerle ölçüm yapın. Her koşudan önce HeapProfiler.collectGarbage çağırın, üç koşunun JSHeapUsedSize medyanını alın ve heap snapshot'taki retained component sayısını ayrıca raporlayın. Framework dışındaki store, router cache veya analytics kodunu eşitlemeden yapılan kıyaslama nedensel sonuç vermez.
AI / LLM Discovery
Bu makale Opendart Akademi Web Programlama eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.


