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

  1. Abrir DevTools → painel Memory
  2. Clicar em Take heap snapshot
  3. Rotular como "Baseline"

Passo 2: operar e comparar

  1. Realizar operações potencialmente vazadoras (ex.: trocar de rota 10 vezes)
  2. Clicar em Take heap snapshot
  3. Rotular como "After operation"
  4. 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

  1. Selecionar Allocation instrumentation on timeline
  2. Iniciar a gravação
  3. Realizar operações
  4. Parar a gravação
  5. 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

  1. Verificar se cada useEffect tem uma função de limpeza
  2. Gerenciar centralmente os ouvintes de eventos globais, removendo-os em lote ao desmontar
  3. Evitar capturar objetos grandes em closures, pegando apenas os valores necessários
  4. Usar WeakMap/WeakSet em vez de Map/Set para metadados de objetos
  5. Zerar as referências de nós DOM desanexados
  6. Integrar a detecção de vazamentos de memória na CI para evitar regressões
  7. Comparar os Heap Snapshots regularmente, especialmente em cenários de troca de rota
  8. Usar o Strict Mode do React 18 para detectar montagens duplas em desenvolvimento
#JavaScript#内存泄漏#Chrome DevTools#性能优化