Publicado em: 29 de outubro de 2026
O Google Chat é um aplicativo de página única grande. Os usuários costumam deixar o app da Web aberto na guia do navegador por dias ou semanas. Em aplicativos de longa duração, a integridade da memória do lado do cliente é uma preocupação principal. As closures não coletadas e as árvores do DOM desanexadas se acumulam lentamente, afetando o desempenho da interface, as pausas de coleta de lixo ou as falhas de guias em dispositivos limitados.
Para resolver isso, a equipe do Google Chat lançou uma iniciativa para descobrir e corrigir problemas de memória. Eles usaram o Chrome DevTools para agentes, que inclui o servidor MCP do DevTools, para transformar um processo manual difícil em um fluxo de trabalho automatizado.
O desafio: uma agulha em um palheiro de 500 MB
Para identificar exatamente por que um objeto vazado é mantido na memória, é necessário rastrear os caminhos de retenção em um código altamente minificado. Embora as melhorias recentes no painel "Memória" do Chrome DevTools (por exemplo, filtragem avançada de bordas e consulta de objetos) facilitem esse processo, fazer isso manualmente em um snapshot de 500 MB ainda pode ser lento e mentalmente exaustivo.
E corrigir um vazamento requer dois conjuntos distintos de conhecimento:
- Você precisa entender como o mecanismo JavaScript V8 lida com o bytecode compilado.
- Você também precisa entender o gerenciamento de estado específico do aplicativo e o ciclo de vida do componente.
A união manual desses dois conjuntos distintos de conhecimento geralmente leva dias de investigação e colaboração com vários desenvolvedores por vazamento.
A solução: navegar pelo heap com um agente
A equipe percebeu que, embora os modelos de linguagem grandes sejam muito bons para raciocinar
sobre dados estruturados, eles precisam das ferramentas certas para processar arquivos enormes e, ao mesmo tempo,
manter o consumo de tokens razoável. Ao conectar o Google
Antigravity ao servidor MCP de código aberto do Chrome DevTools
com as ferramentas de memória ativadas (--memoryDebugging=true), eles deram ao agente
acesso programático direto ao heap do V8.
Em vez de um desenvolvedor expandir manualmente as árvores de retenção no painel de memória do Chrome DevTools, o agente consultou o snapshot diretamente. A equipe usou um comando simples para iniciar o processo:
Discover memory leaks in the heap snapshot chat.heapsnapshot using Chrome DevTools memory tools.
Usando a habilidade de depuração de memória, o agente executou um fluxo de trabalho estruturado usando ferramentas específicas do MCP:
- Capturar ou carregar o snapshot: usando ferramentas como
take_heapsnapshotou lendo um arquivo atual. - Analisou a estrutura do heap: ele procurou fontes comuns de vazamento usando
get_heapsnapshot_detailsecompare_heapsnapshots. - Caminhos de retenção de trace: usou
get_heapsnapshot_retaining_pathspara percorrer a árvore de dominadores de volta às raízes da coleta de lixo. - Gerar correções: ele comparou os caminhos de retenção com o código-fonte do aplicativo para gerar patches altamente específicos.
Insights técnicos: três padrões comuns de vazamento de memória
O agente identificou mais de 30 vazamentos distintos. Os três a seguir são os padrões mais impactantes encontrados pelo agente, representando a maior economia de memória e os antipadrões arquitetônicos mais comuns em aplicativos da Web modernos.
A armadilha de stack trace de erro estático
O agente identificou que o maior vazamento estava vinculado a objetos de erro pré-alocados
que retiveram permanentemente quase 20 MB de memória. Por exemplo, uma única
constante de erro estática (EMPTY_KEY_ERROR) reteve 14,1 MB por conta própria.
O problema: em mecanismos JavaScript modernos, como o V8, a criação de um objeto Error
captura um snapshot da pilha de chamadas atual para ajudar na depuração. O
tamanho retido enorme ocorreu devido a esse mecanismo exato:
- O erro era uma constante global: ele foi instanciado durante a avaliação inicial do módulo.
- O mecanismo JavaScript capturou a pilha: a função ativa na pilha naquele momento era o wrapper de inicialização principal do aplicativo (uma expressão de função imediatamente invocada ou IIFE).
- A matriz reteve a memória: como o aplicativo nunca coletou o lixo do erro, a matriz de stack trace interna manteve uma referência permanente à função de inicialização do pacote principal.
// Ineffective pattern: Instantiating Error objects at module evaluation time
export const EMPTY_KEY_ERROR = new Error("Key cannot be empty");
export function validateKey(key: string) {
if (!key) {
throw EMPTY_KEY_ERROR;
}
}
A correção: a equipe mudou o código para criar erros dinamicamente quando eles são gerados.
Se um erro estático reutilizável for estritamente necessário, limpar explicitamente a pilha
(EMPTY_KEY_ERROR.stack = '';) vai liberar imediatamente os frames internos capturados
do V8.
// Recommended: Construct errors dynamically
export function validateKey(key: string) {
if (!key) {
throw new Error("Key cannot be empty");
}
}
// Or clear the stack property if a static instance is necessary:
export const REUSABLE_ERROR = new Error("Static error");
REUSABLE_ERROR.stack = ""; // Releases the captured initialization frame
Como escalonar listeners de eventos de janela global
O agente encontrou um problema de escalonamento de memória envolvendo listeners de eventos
visibilitychange. Embora isso não seja um "vazamento" estrito (já que eles são limpos
quando o componente é desmontado), o uso da memória aumentou porque havia um
listener para cada mensagem na tela. O snapshot revelou 1.142 listeners de eventos
integrados, com 128 listeners ativos anexados diretamente ao objeto de janela
global.
O problema: em JavaScript, os callbacks de listeners de eventos são closures. Uma closure
retém referências ao escopo léxico que a envolve para garantir que as variáveis
necessárias permaneçam acessíveis. O agente rastreou 126 desses listeners até um
único componente: o RelativeTimeController. Cada carimbo de data/hora na interface do Chat
(por exemplo, "5 minutos atrás") gerenciava o próprio controlador, que registrava um
listener de visibilidade particular para atualizar o horário. Como cada listener ancorou
a instância do controlador à raiz da janela, uma rede densa de closures ativos ocupou
uma quantidade significativa de memória durante a vida útil da conversa visível.
// Ineffective pattern: Attaching global window listeners in individual UI controllers
export class RelativeTimeController {
constructor() {
this.visibilityListener = () => this.updateTime();
window.addEventListener("visibilitychange", this.visibilityListener);
}
// If dispose() fails to run or unregister, the closure pins the controller
}
A correção: a equipe centralizou os eventos de janela global. Um TimerService compartilhado
agora registra exatamente um listener de visibilidade na janela e transmite
atualizações para todos os controladores de carimbo de data/hora ativos. Isso reduziu instantaneamente 126
ouvintes para exatamente 1.
// Recommended: Centralized event coordination using a shared service
export class VisibilityService {
private listeners = new Set<() => void>();
constructor() {
// Single global listener throttled for the entire application
window.addEventListener("visibilitychange", () => this.notifyAll());
}
subscribe(callback: () => void): () => void {
this.listeners.add(callback);
return () => this.listeners.delete(callback); // Unsubscribe handle
}
}
SDK de terceiros e retentores de iframe incorporados
Os nós DOM removidos são elementos que foram removidos da árvore de documentos, mas ainda são referenciados por uma variável JavaScript. Se um único elemento em uma árvore do DOM removida for retido por uma variável raiz, a árvore inteira não poderá ser recuperada.
O problema: quando os usuários navegavam entre os espaços de chat, o agente descobriu que árvores do DOM inteiras permaneciam na memória. Ele rastreou o caminho de retenção de volta para um player de mídia incorporado na interface do espaço de chat. Quando um espaço de chat era desmontado, o componente da interface era removido da página, mas o SDK do player de mídia subjacente nunca era explicitamente destruído. A biblioteca de terceiros mantinha um dicionário global interno de IDs de iframe ativos, mantendo o DOM removido ativo.
A correção: verifique se todos os iframes incorporados e widgets de terceiros estão vinculados aos
ciclos de vida de descarte de componentes. Chamar explicitamente o método .destroy() no
player limpa a entrada do registro global do SDK e permite que o DOM seja
coletado como lixo.
// Recommended: Explicit teardown of embedded frames and SDK instances
export class MediaPlayerController {
private player: ThirdPartyPlayer | null = null;
dispose(): void {
if (this.player) {
this.player.destroy(); // Clears entry from global SDK registry
this.player = null;
}
}
}
Outros padrões
Embora os três padrões mencionados antes tenham sido os mais impactantes em termos de megabytes brutos economizados, o agente também detectou e corrigiu mais de 27 outros vazamentos. Isso incluiu:
- Telemetria de matriz de imagens: em que as árvores do DOM removidas foram mantidas ativas por matrizes de diagnóstico personalizadas que rastreiam métricas de imagem acima da dobra.
- Bugs no ciclo de vida do componente do framework: em que os menus suspensos personalizados elevados ao corpo do documento ignoravam a desmontagem dos pais originais.
- Armadilha de fechamento de objeto de argumentos implícitos: em que os fechamentos internos capturaram acidentalmente toda a matriz de argumentos de funções externas.
Impacto: gerenciamento proativo de memória como um fluxo de trabalho
Ao equipar um agente de programação com o Chrome DevTools para agentes, a equipe do Google Chat automatizou uma tarefa altamente especializada. O rastreamento de um vazamento de memória complexo caiu de dias de travessia manual de heap para minutos de análise automatizada.
Assim como o P2ER implementou a "Verificação empírica obrigatória" para garantir a estabilidade da interface, as equipes podem adotar essa abordagem de auditoria de memória como uma proteção proativa. Os desenvolvedores costumam ignorar problemas de memória, presumindo que eles só acontecem em aplicativos muito grandes ou complexos. No entanto, como mostra este estudo de caso, padrões comuns, como listeners de eventos padrão ou erros estáticos, podem se acumular silenciosamente. Integrar ferramentas como o Chrome DevTools para agentes ao seu ciclo de desenvolvimento diário transforma a IA de um gerador de código básico em um controle de qualidade confiável que detecta regressões silenciosas antes que elas cheguem à produção.