Depuración de fugas de memoria en JavaScript en la práctica: una pipeline completa desde Chrome DevTools hasta la detección automatizada
前端工程
Fugas de memoria: el asesino silencioso de las apps frontend
Las fugas de memoria no causan un fallo inmediato: son como un veneno lento que hace que las páginas se vuelvan progresivamente más lentas hasta que al final quedan en pantalla blanca (white-screen). Esto es especialmente grave en las SPA, donde las páginas nunca se recargan.
| Severidad de la fuga | Síntomas | Escala de tiempo |
|---|---|---|
| Leve | Pequeña lentitud tras uso prolongado | Varias horas |
| Moderada | Lentitud notable tras cambiar de ruta | 30-60 minutos |
| Grave | Pantalla blanca/crash tras interacción | Varios minutos |
Seis patrones clásicos de fuga
1. Temporizadores no limpiados
// 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. Escuchadores de eventos no eliminados
// 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. Referencias accidentales en 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. Nodos DOM desadjuntados (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. Referencias zombie en 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 en lugar 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
Panel de memoria de Chrome DevTools en la práctica
Paso 1: establecer la línea base
- Abrir DevTools → panel de Memory
- Hacer clic en Take heap snapshot
- Etiquetarlo como "Baseline"
Paso 2: operar y comparar
- Realizar operaciones potencialmente con fugas (p. ej., cambiar de ruta 10 veces)
- Hacer clic en Take heap snapshot
- Etiquetarlo como "After operation"
- Seleccionar la vista "Comparison" y comparar con Baseline
Paso 3: analizar objetos en crecimiento
| Columna | Significado |
|---|---|
| Added | Objetos recién creados |
| Deleted | Objetos eliminados |
| Delta | Cambio neto en la cantidad de objetos |
| Allocated Size | Memoria recién asignada |
| Freed Size | Memoria liberada |
| Size Delta | Cambio neto de memoria |
Enfocarse en: objetos con Delta > 0 y Size Delta grande.
Paso 4: examinar los retainers
Clic en un objeto con fuga → ver el panel Retainers → rastrear la cadena de referencias:
Detached HTMLDivElement
↳ retained by Object (componentCache)
↳ retained by Map @12345
↳ retained by Window (global)
Esta cadena indica: un elemento DOM desadjuntado es retenido por el Map componentCache, que es una variable global.
Línea de tiempo de asignación para seguimiento en tiempo real
- Seleccionar Allocation instrumentation on timeline
- Iniciar la grabación
- Realizar operaciones
- Detener la grabación
- Barras azules = asignaciones aún vivas, barras grises = ya recolectadas
Firmas en DevTools de escenarios comunes de fuga
| Tipo de fuga | Firma en Heap Snapshot | Palabras clave |
|---|---|---|
| Escuchadores de eventos | El conteo de EventListener sigue creciendo | eventListeners |
| Temporizadores | Los objetos timer no disminuyen | timer |
| DOM desadjuntado | Detached HTML*Element |
Detached |
| Referencias en closures | closure retiene objetos grandes |
context |
| Map/Set | El tamaño del Map sigue creciendo | Map / Set |
Detección automatizada de fugas de memoria
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);
Integración con 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 de React: lista de verificación de limpieza de 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]);
}
La ayuda del Strict Mode de React 18
En el Strict Mode de React 18, los componentes se montan dos veces y luego se desmontan. Si la limpieza de useEffect es incompleta, la consola revelará el problema de inmediato.
Buenas prácticas
- Revisar cada useEffect en busca de una función de limpieza
- Gestionar centralmente los escuchadores de eventos globales y eliminarlos en lote al desmontar
- Evitar capturar objetos grandes en closures, tomar solo los valores necesarios
- Usar WeakMap/WeakSet en lugar de Map/Set para metadatos de objetos
- Poner a null las referencias de nodos DOM desadjuntados
- Integrar la detección de fugas de memoria en CI para evitar regresiones
- Comparar los Heap Snapshots regularmente, especialmente en escenarios de cambio de ruta
- Usar el Strict Mode de React 18 para detectar dobles montajes en desarrollo
#JavaScript#内存泄漏#Chrome DevTools#性能优化