Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи? Написать
Дайджесты новостей
Иллюстрация к статье: визуализация временного бюджета кадров браузера с разделением задач JavaScript, стилей, отрисовки и веб-воркеров

Экономика главного потока браузера: как укладываться в бюджет кадра и избегать зависаний

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

Главный поток (Main Thread) — это системный поток вкладки браузера, в котором исполняется пользовательский JavaScript, срабатывают обработчики событий, рассчитываются CSS-стили, вычисляется геометрия элементов (Layout) и подготавливаются пиксели для отрисовки (Paint). Все эти задачи делят один цикл событий (Event Loop). Если тяжелая операция блокирует поток, страница перестает реагировать на клики, ввод и прокрутку.

Инженерный анализ Сонхёпа Ли (Sunhyoup Lee) систематизирует методы управления этим ресурсом, формулируя концепцию экономики главного потока: от расчета бюджета времени до применения веб-стандартов кооперативного планирования.

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

Плавность интерфейса измеряется частотой обновления экрана. Для дисплеев с частотой 60 Гц интервал между кадрами составляет 16,6 миллисекунды, а для экранов 120 Гц — 8,3 миллисекунды.

Бюджет кадра (Frame Budget) — это максимальный интервал, в течение которого браузер должен обработать логику и обновить экран. Превышение лимита вызывает пропуск кадров (jank) и дергание анимаций.

Реальный бюджет на JavaScript скромнее номинальных 16,6 мс. Браузеру требуется около 6 миллисекунд на внутренние этапы: пересчет стилей, расчет геометрии и отрисовку. На выполнение пользовательских скриптов остается не более 10 миллисекунд в статичном интерфейсе и не более 5 миллисекунд во время анимаций.

Непрерывная операция JavaScript длительностью более 50 миллисекунд считается длинной задачей (Long Task). Длинные задачи ухудшают ключевые метрики взаимодействия: Total Blocking Time (TBT) и Interaction to Next Paint (INP), отражающую задержку между действием пользователя и визуальным откликом.

Карта симптомов и первичных решений

Для выбора стратегии оптимизации важно различать характер нагрузки:

Симптом проблемыВероятная первопричинаПервичный шаг оптимизацииОграничения и предостережения
Задержка ввода при рендере спискаМонолитная вставка сотен узлов DOMДробление массива на чанки с паузамиОбщее время выполнения не уменьшается
Интерфейс зависает при быстром набореШквал событий keydown с пересчетамиДебаунс (debounce) с отменой старых задачНе подходит для немедленной валидации
Дергается график реального времениОбновления чаще частоты экранаНакопление данных и отрисовка через rAFТребует контроля объема данных в памяти
Замирание страницы при разборе данныхНеделимые расчеты на CPUПеренос вычислений в отдельный Web WorkerВоркер не имеет доступа к DOM и window
Дергается анимация блокаАнимация свойств, вызывающих LayoutПеревод анимаций на transform и opacityТребует проверки слоев композитора

Устранение избыточной нагрузки: батчинг и синхронизация с кадрами

Первое правило оптимизации — устранить лишнюю работу до попыток ее ускорить.

Частые события (сообщения WebSocket, скролл, ввод текста) создают лавину микрозадач. Обновление DOM на каждое событие перегружает браузер повторным расчетом Layout. Решением становится пакетирование (батчинг):

  • Входящие сообщения накапливаются в массиве в памяти.
  • Отрисовка выполняется один раз за кадр через requestAnimationFrame.
  • Для анимаций используются свойства transform и opacity: они передаются композитору и обрабатываются GPU без повтора фаз Layout и Paint.

Кооперативное планирование: эволюция yield от таймеров к scheduler.yield

Если тяжелую работу нельзя вынести из потока (например, создание узлов DOM), ее дробят на фрагменты (чанки) и возвращают управление браузеру (yield), позволяя ему обработать ввод и отрисовать кадр.

Исторически разработчики использовали setTimeout(fn, 0). Однако при вложенных вызовах таймеров стандарт накладывает задержку в 4 миллисекунды. Трюк с MessageChannel обходит задержку, но помещает задачу в конец очереди без учета приоритетов.

Современным стандартом выступает метод scheduler.yield(), разработанный W3C. Он возвращает Promise и временно уступает главный поток. В отличие от таймеров, метод сохраняет приоритет текущей задачи и помещает ее продолжение в начало приоритетной очереди.

Поскольку метод экспериментальный, в продакшене обязательна проверка доступности с отказоустойчивым сценарием:

async function processLargeArray(items, signal) {
  let index = 0;
  while (index < items.length && !signal.aborted) {
    const started = performance.now();
    while (index < items.length && performance.now() - started < 5) {
      renderItem(items[index++]);
    }
    if (index < items.length) {
      if (globalThis.scheduler?.yield) {
        await scheduler.yield();
      } else {
        await new Promise(resolve => requestAnimationFrame(resolve));
      }
    }
  }
}

Такое разделение позволяет браузеру быстро реагировать на действия пользователя прямо во время отрисовки списка.

Диспетчеризация задач через scheduler.postTask

Вместе с планированием применяется API приоритизации scheduler.postTask(). Он разделяет задачи по трем уровням:

  1. user-blocking — критические операции, блокирующие реакцию на ввод.
  2. user-visible — стандартный уровень для обновления видимой области.
  3. background — фоновые операции (отправка аналитики, предварительная загрузка).

В связке с AbortController интерфейс исключает лишние расчеты: если пользователь ввел новый поисковый запрос, фоновая фильтрация старого списка прерывается по сигналу отмены.

Вынос тяжелых вычислений: архитектура Web Workers и Transferable Objects

Ресурсоемкие математические вычисления (шифрование, парсинг, обработка изображений) необходимо изолировать в фоновом потоке через Web Workers.

Веб-воркер исполняется в изолированном контексте: без доступа к DOM и переменным окна. Связь с главным потоком осуществляется через сообщения методом postMessage.

Стандартный postMessage глубоко клонирует данные (structured clone). Передача массива в 50 МБ приводит к сериализации и затратам памяти в главном потоке.

Решением служат перемещаемые объекты — Transferable Objects. Структуры данных вроде ArrayBuffer передаются с мгновенным перемещением права владения (zero-copy transfer):

const pixelBuffer = new ArrayBuffer(1024 * 1024 * 4);
worker.postMessage({ buffer: pixelBuffer }, [pixelBuffer]);
// Исходный buffer становится отсоединенным (detached)
console.log(pixelBuffer.byteLength); // 0

Указание буфера во втором аргументе postMessage передает указатель на память в воркер за доли микросекунды. Главный поток теряет доступ к буферу, исключая состояния гонки.

Пошаговый регламент аудита производительности

Инженерный чеклист профилирования включает пять шагов:

  1. Запись профиля. Открыть вкладку Performance в DevTools и записать профиль при проблемном сценарии.
  2. Локализация длинных задач. Найти красные блоки Long Task (>50 мс) и определить стек вызовов, блокирующий поток.
  3. Оценка рендеринга. Если фазы Layout и Recalculate Style длятся больше 6 мс, оптимизировать селекторы и свойства анимаций.
  4. Кооперативный чанкинг. Разбить цикл на интервалы по 5–10 мс через scheduler.yield() с синхронизацией через rAF.
  5. Вынос вычислений в воркер. Перенести вычисления без DOM в Web Worker, настроив передачу бинарных данных через Transferable Objects, и повторно проверить показатели INP и TBT.

Архитектурная гигиена: измерение важнее микрооптимизаций

Управление главным потоком требует взвешенности. Дробление задач через yield не ускоряет общие вычисления: оно повышает отзывчивость ценой небольших накладных расходов планировщика. Перенос в воркеры оправдан только для тяжелых вычислений, иначе затраты на обмен сообщениями превысят выгоду.

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