[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-09 21:08:02 +09:00
parent f0befc887a
commit 93ec7e9056
363 changed files with 68333 additions and 64 deletions
@@ -0,0 +1,272 @@
---
id: perf-web-memory-leak
title: Web Memory Leak — Detached DOM / Listener / Closure
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [performance, memory, web, vibe-coding]
tech_stack: { language: "TS / Browser", applicable_to: ["Frontend"] }
applied_in: []
aliases: [memory leak, detached DOM, event listener leak, weakref, heap snapshot, Chrome Memory]
---
# Web Memory Leak
> SPA 가 시간이 지나며 느려지면 leak. **Detached DOM, listener, timer, closure, global**. Chrome DevTools Memory tab 으로 추적.
## 📖 핵심 개념
- Detached DOM: 화면에서 제거됐는데 JS 가 보유.
- Closure: 함수가 외부 스코프 보유 (큰 객체 잡을 수 있음).
- Listener: removeEventListener 안 함.
- Timer: clearInterval 안 함.
## 💻 코드 패턴
### Heap snapshot (Chrome)
```
DevTools → Memory → Heap snapshot
1. Snapshot
2. 동작 (페이지 N번 이동)
3. Snapshot 다시
4. Comparison: "Allocated between snapshots"
5. Detached DOM 검색
```
### Performance.measureUserAgentSpecificMemory
```ts
if ('measureUserAgentSpecificMemory' in performance) {
const result = await performance.measureUserAgentSpecificMemory();
console.log(result.bytes);
}
```
### Detached DOM 패턴
```ts
// ❌
let cachedNodes = [];
function build() {
const div = document.createElement('div');
document.body.appendChild(div);
cachedNodes.push(div); // leak — 영원 array 안
div.remove(); // DOM 제거, but cachedNodes 가 잡음
}
// ✅
cachedNodes.length = 0; // 명시 cleanup
// 또는 WeakRef
```
### Listener leak
```ts
// ❌
function attach() {
window.addEventListener('resize', handler); // 매 호출 마다 listener
}
// ✅
const handler = () => { ... };
window.addEventListener('resize', handler);
// cleanup
window.removeEventListener('resize', handler);
// React
useEffect(() => {
const handler = () => { ... };
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
// AbortController (modern)
useEffect(() => {
const ctrl = new AbortController();
window.addEventListener('resize', handler, { signal: ctrl.signal });
return () => ctrl.abort();
}, []);
```
### Timer / interval
```tsx
// ❌
setInterval(tick, 1000); // 영원
// ✅
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
// requestAnimationFrame
useEffect(() => {
let raf: number;
const tick = () => { update(); raf = requestAnimationFrame(tick); };
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
```
### Closure 잡힘
```ts
function createHandler(bigData: any[]) {
return () => console.log('clicked');
// 함수 안 bigData 사용 안 해도 — 일부 V8 가 closure 보존
// 명시적 nullify:
}
function createHandler2() {
let bigData = loadBig();
const handler = () => console.log('clicked');
bigData = null!; // 명시
return handler;
}
```
### WeakMap / WeakRef
```ts
// 객체 키 — GC 가능
const meta = new WeakMap<HTMLElement, Metadata>();
meta.set(el, { ... });
// el 이 DOM 에서 제거 + 다른 ref 없음 → 자동 cleanup
// WeakRef (advanced)
const ref = new WeakRef(largeObject);
// 사용
const obj = ref.deref();
if (obj) obj.method();
// largeObject 가 다른 곳 참조 없으면 GC 가능
```
### Cache (TTL)
```ts
import LRU from 'lru-cache';
const cache = new LRU<string, Response>({
max: 500, // 최대 N개
ttl: 5 * 60_000, // 5분
});
```
→ Map 무한 자라남 방지.
### React 흔한 leak
```tsx
// ❌ 비동기 + 컴포넌트 unmount
useEffect(() => {
fetch('/data').then(r => r.json()).then(d => setState(d));
// unmount 후 setState — warning + 그냥 leak 까진 X
}, []);
// ✅ AbortController
useEffect(() => {
const ctrl = new AbortController();
fetch('/data', { signal: ctrl.signal }).then(r => r.json()).then(setState).catch(() => {});
return () => ctrl.abort();
}, []);
// 또는 isMounted (legacy)
let mounted = true;
fetch(...).then(d => mounted && setState(d));
return () => { mounted = false; };
```
### Subscription leak
```tsx
// ❌
useEffect(() => {
store.subscribe(setState);
// unsubscribe 없음
}, []);
// ✅
useEffect(() => {
const unsub = store.subscribe(setState);
return unsub;
}, []);
```
### Detection (long-running app)
```ts
// 주기적 메모리 체크
setInterval(() => {
if ((performance as any).memory) {
const m = (performance as any).memory;
log('mem', { used: m.usedJSHeapSize, limit: m.jsHeapSizeLimit });
}
}, 60_000);
```
⚠️ Chrome only, behind flag.
### Big binary (image / video) 정리
```ts
// Object URL
const url = URL.createObjectURL(blob);
img.src = url;
// 사용 후
URL.revokeObjectURL(url);
// Canvas / video
videoEl.pause();
videoEl.src = '';
videoEl.load();
```
### Service Worker 가 cache 누적
```ts
// SW
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CURRENT).map(k => caches.delete(k)))
)
);
});
// 또는 Workbox expiration
```
### MutationObserver / ResizeObserver
```ts
// ❌ disconnect 안 함
const obs = new ResizeObserver(...);
obs.observe(el);
// component unmount → obs 가 el 참조 + el 가 obs 참조
// ✅
useEffect(() => {
const obs = new ResizeObserver(...);
obs.observe(ref.current!);
return () => obs.disconnect();
}, []);
```
## 🤔 의사결정 기준
| 의심 | 도구 |
|---|---|
| 시간 지나며 느려짐 | Heap snapshot diff |
| 특정 page 후 큼 | Allocation timeline |
| Listener 의심 | Event Listeners panel |
| Detached DOM | Memory > Comparison > Detached |
| Long task | Performance > 시간 측정 |
| 자동 production | Sentry / Datadog memory metric |
## ❌ 안티패턴
- **`useEffect` cleanup 없음**: listener / timer / sub leak.
- **Global cache 무한**: TTL / LRU.
- **DOM 직접 보유**: WeakRef / cleanup.
- **AbortController 없는 fetch**: unmount 후 setState.
- **Object URL revoke 안 함**: 큰 image / video.
- **SW cache 무한**: expiration.
- **React DevTools 측정 안 함**: 추측만.
## 🤖 LLM 활용 힌트
- 모든 effect 에 cleanup.
- AbortController + signal 패턴 표준.
- Heap snapshot diff 가 leak 추적.
## 🔗 관련 문서
- [[Native_Memory_Profiling]]
- [[React_useEffect_Pitfalls]]
- [[Web_Service_Worker_Patterns]]