Depuração de vazamentos de memória em JavaScript na prática: um pipeline completo do Chrome DevTools à detecção automatizada
前端工程
Vazamentos de memória: o assassino silencioso das aplicações frontend
Os vazamentos de memória não causam falha imediata — são como um veneno lento que deixa as páginas progressivamente mais lentas até que, no fim, fiquem em tela branca (white-screen). Isso é especialmente grave em SPAs, onde as páginas nunca são recarregadas.
| Gravidade do vazamento | Sintomas | Escala de tempo |
|---|---|---|
| Leve | Pequena lentidão após uso prolongado | Várias horas |
| Moderado | Lentidão perceptível após troca de rota | 30-60 minutos |
| Grave | Tela branca/crash após interação | Vários minutos |
Seis padrões clássicos de vazamento
1. Temporizadores não limpos
// Leak: timer keeps running after component unmounts
function PollingComponent() {
useEffect(() => {
const timer = setInterval(fetchData, 5000);
// Forgot to return cleanup
}, []);
}
// Fix
function PollingComponent() {
useEffect(() => {
const timer = setInterval(fetchData, 5000);
return () => clearInterval(timer);
}, []);
}
2. Ouvintes de eventos não removidos
// Leak: global event listener holds component reference
function ScrollComponent() {
useEffect(() => {
window.addEventListener('scroll', handleScroll);
// Forgot to remove
}, []);
}
// Fix
function ScrollComponent() {
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
}
3. Referências acidentais em closures
// Leak: closure holds reference to entire largeData
function processChunk(largeData) {
const result = largeData.items[0].value;
// Even though only result is used, closure still holds largeData
return function getResult() {
return result;
};
}
// Fix: only capture the needed value
function processChunk(largeData) {
const result = largeData.items[0].value;
const captured = result; // primitive type, no reference
return function getResult() {
return captured;
};
}
4. Nós DOM desanexados (Detached)
// Leak: DOM removed but JS still references it
const element = document.getElementById('card');
element.remove();
// element variable still holds reference, DOM node cannot be garbage collected
// Fix: null out the reference after removal
element.remove();
element = null;
5. Referências zumbi em Map/Set
// Leak: Map holds references to destroyed components
const componentCache = new Map();
function register(id, component) {
componentCache.set(id, component);
}
// Component destroyed but not removed from Map
// Fix: clean up on destroy
function unregister(id) {
componentCache.delete(id);
}
6. Usar Map em vez de WeakMap
// When keys are objects and auto-collection is desired, use WeakMap
const metadata = new WeakMap();
function attachMeta(obj, meta) {
metadata.set(obj, meta); // when obj is GC'd, entry auto-disappears
}
// Using Map, the entry persists even after obj is GC'd
Painel de memória do Chrome DevTools na prática
Passo 1: estabelecer a linha de base
- Abrir DevTools → painel Memory
- Clicar em Take heap snapshot
- Rotular como "Baseline"
Passo 2: operar e comparar
- Realizar operações potencialmente vazadoras (ex.: trocar de rota 10 vezes)
- Clicar em Take heap snapshot
- Rotular como "After operation"
- Selecionar a visualização "Comparison" e comparar com a linha de base
Passo 3: analisar objetos em crescimento
| Coluna | Significado |
|---|---|
| Added | Objetos recém-criados |
| Deleted | Objetos excluídos |
| Delta | Variação líquida na contagem de objetos |
| Allocated Size | Memória recém-alocada |
| Freed Size | Memória liberada |
| Size Delta | Variação líquida de memória |
Foque em: objetos com Delta > 0 e Size Delta grande.
Passo 4: examinar os retainers
Clicar em um objeto vazado → abrir o painel Retainers → rastrear a cadeia de referências:
Detached HTMLDivElement
↳ retained by Object (componentCache)
↳ retained by Map @12345
↳ retained by Window (global)
Essa cadeia mostra: um elemento DOM desanexado é retido pelo Map componentCache, que é uma variável global.
Linha de tempo de alocação para acompanhamento em tempo real
- Selecionar Allocation instrumentation on timeline
- Iniciar a gravação
- Realizar operações
- Parar a gravação
- Barras azuis = alocações ainda vivas, barras cinzas = já coletadas
Assinaturas do DevTools para cenários comuns de vazamento
| Tipo de vazamento | Assinatura no Heap Snapshot | Palavras-chave |
|---|---|---|
| Ouvintes de eventos | A contagem de EventListener continua crescendo | eventListeners |
| Temporizadores | Objetos timer não diminuem | timer |
| DOM desanexado | Detached HTML*Element |
Detached |
| Referências em closure | closure retém objetos grandes |
context |
| Map/Set | O tamanho do Map continua crescendo | Map / Set |
Detecção automatizada de vazamentos de memória
Puppeteer + DevTools Protocol
const puppeteer = require('puppeteer');
async function detectMemoryLeak(url, action, iterations = 10) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url);
const results = [];
for (let i = 0; i < iterations; i++) {
// Perform operation
await action(page);
// Force GC
await page.evaluate(() => {
if (window.gc) window.gc();
});
// Get memory metrics
const metrics = await page.metrics();
results.push({
iteration: i + 1,
jsHeapUsedSize: metrics.JSHeapUsedSize,
});
}
await browser.close();
// Analyze trend: if heap keeps growing, there may be a leak
const first = results[0].jsHeapUsedSize;
const last = results[results.length - 1].jsHeapUsedSize;
const growth = (last - first) / first;
return {
leaked: growth > 0.1, // >10% growth considered a leak
growthRate: `${(growth * 100).toFixed(1)}%`,
details: results,
};
}
// Usage
const result = await detectMemoryLeak(
'http://localhost:3000',
async (page) => {
await page.click('#navigate-btn');
await page.waitForSelector('#content');
await page.click('#back-btn');
},
20
);
console.log(result);
Integração com CI
# .github/workflows/memory-check.yml
name: Memory Leak Check
on: [push]
jobs:
memory-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- run: node scripts/memory-leak-test.js
Específico do React: checklist de limpeza do useEffect
function DataComponent({ id }) {
useEffect(() => {
const controller = new AbortController();
const timer = setInterval(() => {}, 5000);
const handler = () => {};
window.addEventListener('resize', handler);
const subscription = eventBus.subscribe('event', handler);
return () => {
controller.abort(); // Cancel request
clearInterval(timer); // Clear timer
window.removeEventListener('resize', handler); // Remove listener
subscription.unsubscribe(); // Unsubscribe
};
}, [id]);
}
A ajuda do Strict Mode do React 18
No Strict Mode do React 18, os componentes são montados duas vezes e depois desmontados. Se a limpeza do useEffect estiver incompleta, o console revelará o problema imediatamente.
Boas práticas
- Verificar se cada useEffect tem uma função de limpeza
- Gerenciar centralmente os ouvintes de eventos globais, removendo-os em lote ao desmontar
- Evitar capturar objetos grandes em closures, pegando apenas os valores necessários
- Usar WeakMap/WeakSet em vez de Map/Set para metadados de objetos
- Zerar as referências de nós DOM desanexados
- Integrar a detecção de vazamentos de memória na CI para evitar regressões
- Comparar os Heap Snapshots regularmente, especialmente em cenários de troca de rota
- Usar o Strict Mode do React 18 para detectar montagens duplas em desenvolvimento
#JavaScript#内存泄漏#Chrome DevTools#性能优化