Как в Google Chat исправили более 30 проблем с памятью, используя инструменты разработчика Chrome для агентов

Vatan Aggarwal
Vatan Aggarwal Software Engineer

Дата публикации: 29 октября 2026 г.

Google Chat – это большое одностраничное приложение. Пользователи часто оставляют веб-приложение открытым на вкладке браузера в течение нескольких дней или недель. В долгоживущих приложениях состояние памяти на стороне клиента является основной проблемой. Несобранные замыкания и отсоединенные деревья DOM накапливаются медленно, в конечном итоге влияя на производительность пользовательского интерфейса, паузы сборки мусора или сбои вкладок на ограниченных устройствах.

Чтобы решить эту проблему, команда Google Chat запустила инициативу по обнаружению и устранению проблем с памятью. Они использовали инструменты разработчика Chrome для агентов, в том числе сервер MCP, чтобы превратить сложный ручной процесс в автоматизированный рабочий процесс.

Задача: иголка в стоге сена размером 500 МБ

Чтобы точно определить, почему объект с утечкой остается в памяти, необходимо отследить пути сохранения в сильно минифицированном коде. Недавние улучшения панели "Память" в инструментах разработчика Chrome, например расширенная фильтрация ребер и запросы объектов, упрощают эту задачу, но вручную анализировать снимок размером 500 МБ все равно может быть долго и утомительно.

А для устранения утечки требуются два разных набора знаний:

  • Вам нужно понять, как движок V8 JavaScript обрабатывает скомпилированный байт-код.
  • Вам также необходимо понимать, как в вашем приложении устроено управление состоянием и жизненный цикл компонентов.

Чтобы вручную сопоставить эти два набора данных, обычно требуется несколько дней исследований и совместной работы с несколькими разработчиками.

Решение: навигация по куче с помощью агента

Команда поняла, что хотя большие языковые модели отлично справляются с анализом структурированных данных, им нужны правильные инструменты для обработки огромных файлов при разумном потреблении токенов. Подключив Google Antigravity к серверу MCP инструментов разработчика в Chrome с открытым исходным кодом и включенными инструментами памяти (--memoryDebugging=true), они предоставили агенту прямой программный доступ к куче V8.

Вместо того чтобы вручную разворачивать деревья объектов в панели памяти инструментов разработчика Chrome, агент напрямую запрашивал снимок. Для начала команда использовала простой запрос:

Discover memory leaks in the heap snapshot chat.heapsnapshot using Chrome DevTools memory tools.

Используя навык отладки памяти, агент выполнил структурированный рабочий процесс с помощью определенных инструментов MCP:

  1. Сделайте или загрузите снимок. Используйте такие инструменты, как take_heapsnapshot, или прочитайте существующий файл.
  2. Проанализировал структуру кучи. С помощью get_heapsnapshot_details и compare_heapsnapshots он нашел распространенные источники утечек.
  3. Отслеживание путей сохранения. С помощью get_heapsnapshot_retaining_paths можно было пройти по дереву доминаторов обратно к корням сборки мусора.
  4. Генерация исправлений. ИИ сопоставил пути удержания с исходным кодом приложения и создал точечные исправления.

Технические сведения: три распространенных шаблона утечки памяти

Агент обнаружил более 30 утечек. Ниже приведены три наиболее эффективных шаблона, найденных агентом, которые представляют собой наибольшую экономию памяти и наиболее распространенные архитектурные антишаблоны в современных веб-приложениях.

Ловушка трассировки стека статических ошибок

Агент определил, что самая большая утечка была связана с предварительно выделенными объектами ошибок, которые постоянно удерживали почти 20 МБ памяти. Например, одна статическая константа ошибки (EMPTY_KEY_ERROR) занимала 14,1 МБ.

Проблема. В современных движках JavaScript, таких как V8, при создании объекта Error создается снимок текущего стека вызовов, который помогает в отладке. Огромный сохраненный размер возник именно из-за этого механизма:

  • Ошибка была глобальной константой. Она была создана во время первоначальной оценки модуля.
  • Движок JavaScript зафиксировал стек. Активной функцией в стеке на тот момент была основная оболочка инициализации приложения (немедленно вызываемое функциональное выражение, или IIFE).
  • Массив заблокировал память. Поскольку приложение не удаляло ошибку, массив трассировки стека постоянно ссылался на функцию инициализации основного пакета.
// 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;
  }
}

Решение. Команда изменила код, чтобы ошибки создавались динамически при их возникновении. Если многократно используемая статическая ошибка строго необходима, явная очистка стека (EMPTY_KEY_ERROR.stack = '';) немедленно освобождает захваченные внутренние фреймы 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

Масштабирование глобальных прослушивателей событий окна

Агент обнаружил проблему с масштабированием памяти, связанную с visibilitychange прослушивателями событий. Хотя это и не является утечкой в строгом смысле (поскольку они в конечном итоге очищаются при размонтировании компонента), это увеличивало использование памяти, поскольку для каждого сообщения на экране был один слушатель. Снимок показал 1142 встроенных прослушивателя событий, из которых 128 активных прослушивателей были прикреплены непосредственно к глобальному объекту window.

Проблема. В JavaScript обратные вызовы прослушивателей событий являются замыканиями. Замыкание сохраняет ссылки на свою лексическую область видимости, чтобы переменные, которые ему нужны, оставались доступными. Агент отследил 126 из этих прослушивателей до одного компонента: RelativeTimeController. Каждая временная метка в интерфейсе Chat (например, "5 минут назад") управлялась собственным контроллером, который регистрировал частный прослушиватель видимости для обновления времени. Поскольку каждый прослушиватель привязывал свой экземпляр контроллера к корню окна, плотная сеть активных замыканий связывала значительный объем памяти на протяжении всего времени существования видимой цепочки чата.

// 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
}

Решение. Команда централизовала глобальные события окна. Общий объект TimerService теперь регистрирует в окне ровно один прослушиватель видимости и передает обновления всем активным контроллерам временных меток. Это мгновенно сократило 126 слушателей до одного.

// 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 и встроенные iframe-контейнеры

Отделенные узлы DOM – это элементы, которые были удалены из дерева документа, но на которые по-прежнему ссылается переменная JavaScript. Если корневая переменная сохраняет хотя бы один элемент в отделенном дереве DOM, то все дерево не может быть удалено.

Проблема. Когда пользователи переходили между чат-группами, агент обнаружил, что целые отсоединенные деревья DOM оставались в памяти. Она отследила путь сохранения до встроенного медиапроигрывателя в интерфейсе чат-группы. При отключении чат-группы компонент интерфейса удалялся со страницы, но базовый SDK медиапроигрывателя не уничтожался. Сторонняя библиотека поддерживала внутренний глобальный словарь идентификаторов активных iframe, сохраняя отделенный DOM.

Решение. Убедитесь, что все встроенные элементы iframe и сторонние виджеты связаны с жизненными циклами удаления компонентов. Явный вызов метода .destroy() для проигрывателя удаляет запись из глобального реестра SDK и позволяет DOM выполнить сборку мусора.

// 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;
    }
  }
}

Дополнительные шаблоны

Хотя три упомянутых выше шаблона были наиболее эффективными с точки зрения сэкономленных мегабайт, агент также обнаружил и устранил более 27 других утечек. Вот некоторые из них:

  • Телеметрия массива изображений. Отделенные деревья DOM поддерживались в активном состоянии специальными диагностическими массивами, отслеживающими показатели изображений в верхней части страницы.
  • Ошибки жизненного цикла компонентов фреймворка. Специальные раскрывающиеся меню, поднятые до тела документа, обходили размонтирование своих исходных родительских элементов.
  • Ловушка неявного замыкания объекта arguments. Внутренние замыкания непреднамеренно захватывали весь массив arguments внешних функций.

Результат: упреждающее управление памятью как рабочий процесс

Оснастив агента-программиста инструментами разработчика Chrome для агентов, команда Google Chat автоматизировала узкоспециализированную задачу. Поиск сложной утечки памяти сократился с нескольких дней ручного обхода кучи до нескольких минут автоматизированного анализа.

Подобно тому, как в P2ER была реализована обязательная эмпирическая проверка для обеспечения стабильности пользовательского интерфейса, команды могут использовать этот подход к аудиту памяти в качестве упреждающей меры. Разработчики часто игнорируют проблемы с памятью, полагая, что они возникают только в очень больших или сложных приложениях. Однако, как показывает этот пример, распространенные шаблоны, такие как стандартные прослушиватели событий или статические ошибки, могут незаметно накапливаться. Интеграция таких инструментов, как инструменты разработчика Chrome для агентов, в ежедневный цикл разработки превращает ИИ из простого генератора кода в надежный инструмент контроля качества, который выявляет скрытые регрессии до того, как они попадут в рабочую среду.