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

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

Исун-Хёп Ли написал большой практический разбор главного потока с интерактивными демо к каждому приёму. Идея простая: JavaScript, расчёт стилей, layout, paint, обработка событий и внутренности фреймворка стоят в одной очереди на одном потоке. На дисплее 60 Гц на кадр даётся около 16,6 мс, а после вычета собственных затрат браузера реальный бюджет — порядка 10 мс. Одна функция на 200 мс означает 200 мс без перерисовки и без отклика на клики. INP и TBT по сути просто измеряют, сколько этот поток был заблокирован.

Первая половина статьи — про разумные траты этого потока, четырьмя ходами:

→ **Splitting (разбиение)** — режем долгую работу на куски и отдаём поток между ними. Yield не делает работу быстрее (суммарное время даже чуть растёт). Он создаёт промежутки между задачами, а рендер и отложенный ввод могут выполниться только между задачами. Поток чата, отрисованный пачками по 20 сообщений с \await new Promise(r => setTimeout(r, 0)), вернул жизнь полю ввода → **Разбиение по времени** для работы рядом с анимацией — работаем 5 мс и продолжаем на следующем \requestAnimationFrame, отсчитывая от времени начала кадра, чтобы несколько анимаций могли делить один кадр → **Batching (пакетирование)** — debounce для markdown-превью, которое перестраивает документ на 2 000 строк, и слияние обновлений через \requestAnimationFrame\ для табло из 60 тикеров, которое получает больше 1 000 сообщений в секунду: все данные сохраняем, рисуем один раз за кадр → **Prioritizing (приоритизация)** — самописная очередь на \MessageChannel, в которой срочные задачи проходят без очереди. В демо 60 прикреплённых фото генерируют превью в фоне, а клик по неготовой плитке переносит её задачу в начало очереди

Самое честное в статье — ограничения: если дробить слишком мелко, накладные расходы на yield перевесят саму работу, у \setTimeout\ есть минимальная задержка, а \JSON.parse\ на несколько мегабайт — это один атомарный вызов, который просто нельзя разбить. \scheduler.yield()\ помогает (после него работа продолжается раньше других задач в очереди), но поддержка пока неровная.

Дальше в статье — откладывание работы и вторая семья решений: вообще не использовать главный поток — compositor, воркеры или полное устранение работы. Намёк даёт уже первое демо: нажмёшь кнопку блокировки — JS-анимация и ввод замирают, а CSS-анимация продолжает крутиться.

Полная статья с интерактивными демо у Lee Sun-Hyoup 👇 https://kciter.so/posts/the-expensive-main-thread/en/

#frontend #javascript #performance #webdew #inp