Дайджесты новостей
Графическая сцена детективного расследования бага в Chrome с секундомером аномалии 89s, заблокированной очередью IPC и ускорением до 289ms.

Расследование зависания попапа Chrome-расширения в Циан: от 89 секунд до 289 мс

В инженерной практике нет ничего более захватывающего, чем расследование аномальных багов производительности. Одно дело, когда приложение тормозит из-за очевидного цикла на миллион итераций, и совсем другое — когда профайлер показывает нулевую загрузку процессора, а интерфейс открывается мучительные 89 секунд.

Именно в такой технический детектив погрузился фронтенд-инженер компании Циан Анатолий Барцев при поддержке внутреннего Chrome-расширения, которым ежедневно пользуются более 200 сотрудников компании. Похожие тонкости взаимодействия расширений и ядра Chromium мы регулярно отслеживаем в обзорах вроде Браузерные движки и веб-стандарты: новшества Chrome 155 и специфика Safari на складных экранах.

Симптомы аномалии и крах очевидных гипотез

Расширение построено по современному стандарту Chrome Extensions Manifest V3: фоновый Service Worker, всплывающее окно (Popup) на React 18 и библиотека компонентов Ant Design.

В один прекрасный день пользователи начали массово жаловаться: кликаешь по значку расширения в панели Chrome — и браузер замирает. Всплывающее окно появляется ровно через 89 035 миллисекунд (почти полторы минуты!).

Первые стандартные гипотезы инженеров быстро рассыпались:

  1. Тяжелый бандл? Размер JS составлял 1.96 МБ. Анализ DevTools показал, что чтение с SSD и компиляция скрипта занимают всего 18 мс.
  2. Тормозит рендеринг компонентов? До фазы монтирования React дело даже не доходило: событие DOMContentLoaded не наступало больше минуты.
  3. Утечка памяти в сервис-воркере? Воркер спокойно спал и потреблял копеечные 25 МБ RAM.

Корень зла: скрытый IPC-шторм в chrome.storage

Разгадка обнаружилась при анализе низкоуровневых трейсов Chrome (chrome://tracing).

В архитектуре Manifest V3 хранилище chrome.storage.local — это не синхронный localStorage. Это межпроцессный асинхронный канал (IPC, Inter-Process Communication), работающий поверх сериализации JSON между процессом расширения, браузерным процессом и фоновым воркером.

В коде расширения жил невинный слушатель событий изменения настроек:

// Старый код с критической уязвимостью архитектуры
chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName !== 'local') return;
  if (changes.enabled) {
    // ОШИБКА: безусловная повторная запись значения!
    chrome.storage.local.set({ enabled: changes.enabled.newValue });
  }
});

Ловушка заключалась в спецификации Chromium API: метод chrome.storage.local.set() порождает событие onChanged ВСЕГДА, даже если ты записываешь абсолютно идентичное значение (например, повторно пишешь enabled: false).

В итоге возник замкнутый цикл: запись порождала событие, обработчик снова вызывал запись, порождая следующее событие. За рабочий день в скрытой системной очереди IPC браузера скапливалось более 250 000 необработанных сообщений! Когда пользователь кликал на попап, браузер добросовестно пытался сначала обработать эту четверть миллиона пустых IPC-пакетов, блокируя инициализацию окна.

Однострочник, ускоривший запуск в 300 раз

Инженерное решение потребовало добавления защитного барьера с проверкой перехода состояния (State Transition Guard):

// background.js / service-worker.js
chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName !== 'local') return;

  if (changes.enabled) {
    const { oldValue, newValue } = changes.enabled;

    // КРИТИЧЕСКИЙ GUARD: отсекаем идентичные записи!
    if (oldValue === newValue) {
      return;
    }

    console.log("Статус изменен с " + oldValue + " на " + newValue);
    updateExtensionBadge(newValue);
  }
});

А для точного контроля времени старта в скрипт попапа добавили замер задержки:

// popup.js
const startTime = performance.now();

document.addEventListener('DOMContentLoaded', () => {
  const duration = performance.now() - startTime;
  console.log("Холодный старт попапа: " + duration.toFixed(1) + " мс");
  chrome.runtime.sendMessage({ type: 'POPUP_METRIC', duration });
});
Особенность chrome.storage в Manifest V3

Никогда не надейтесь на внутреннюю проверку равенства данных браузером. API chrome.storage не сверяет глубокое равенство объектов: любой вызов .set() будет транслирован в IPC-событие. Всегда проверяйте oldValue !== newValue вручную.

После выкатки патча время открытия всплывающего окна мгновенно вернулось с 89 секунд к штатным 289 миллисекундам. Блестящий пример того, как глубокое понимание архитектуры платформы решает сложнейшие загадки продакшена.