В инженерной практике нет ничего более захватывающего, чем расследование аномальных багов производительности. Одно дело, когда приложение тормозит из-за очевидного цикла на миллион итераций, и совсем другое — когда профайлер показывает нулевую загрузку процессора, а интерфейс открывается мучительные 89 секунд.
Именно в такой технический детектив погрузился фронтенд-инженер компании Циан Анатолий Барцев при поддержке внутреннего Chrome-расширения, которым ежедневно пользуются более 200 сотрудников компании. Похожие тонкости взаимодействия расширений и ядра Chromium мы регулярно отслеживаем в обзорах вроде Браузерные движки и веб-стандарты: новшества Chrome 155 и специфика Safari на складных экранах.
Симптомы аномалии и крах очевидных гипотез
Расширение построено по современному стандарту Chrome Extensions Manifest V3: фоновый Service Worker, всплывающее окно (Popup) на React 18 и библиотека компонентов Ant Design.
В один прекрасный день пользователи начали массово жаловаться: кликаешь по значку расширения в панели Chrome — и браузер замирает. Всплывающее окно появляется ровно через 89 035 миллисекунд (почти полторы минуты!).
Первые стандартные гипотезы инженеров быстро рассыпались:
- Тяжелый бандл? Размер JS составлял 1.96 МБ. Анализ DevTools показал, что чтение с SSD и компиляция скрипта занимают всего 18 мс.
- Тормозит рендеринг компонентов? До фазы монтирования React дело даже не доходило: событие
DOMContentLoadedне наступало больше минуты. - Утечка памяти в сервис-воркере? Воркер спокойно спал и потреблял копеечные 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 });
});
Никогда не надейтесь на внутреннюю проверку равенства данных браузером. API chrome.storage не сверяет глубокое равенство объектов: любой вызов .set() будет транслирован в IPC-событие. Всегда проверяйте oldValue !== newValue вручную.
После выкатки патча время открытия всплывающего окна мгновенно вернулось с 89 секунд к штатным 289 миллисекундам. Блестящий пример того, как глубокое понимание архитектуры платформы решает сложнейшие загадки продакшена.
