
Основной поток браузера — дорогое удовольствие
Когда думают про оптимизацию фронтенда - вспоминают про сеть, бандл, кэш. Сократить запросы, уменьшить размер JS, настроить CDN. Это правильно и это работает.
Но есть вещь которую почти никто не упоминает - основной поток браузера. И есть причина почему о нём не говорят: на большинстве страниц он не становится проблемой.
Ровно до того момента когда становится.
Что такое основной поток
Браузер делает всё в одном потоке: разбирает HTML, выполняет JavaScript, реагирует на клики, обрабатывает анимации. Пока этот поток занят - страница не реагирует ни на что. Скролл зависает. Кнопки не нажимаются. Анимации дёргаются.
На простых страницах это незаметно - задачи короткие, поток успевает. Но стоит добавить сложную логику, стриминг данных, тяжёлые вычисления - и пользователь начинает чувствовать что сайт «тормозит».
При этом сеть работает нормально. Бандл маленький. Lighthouse зелёный. А всё равно что-то не так.
Шесть способов разгрузить поток
Splitting - разбить тяжёлую задачу на части. Если нужно обработать тысячу элементов - не делай это за один проход. Разбей на чанки по 50, давай потоку дышать между ними через setTimeout или scheduler.yield(). Пользователь не заметит паузы в 5мс, но заметит что интерфейс перестал реагировать на секунду.
Batching - группировать обновления. Вместо того чтобы обновлять DOM после каждого входящего события - собери изменения и примени разом. React делает это автоматически, но если работаешь напрямую с DOM или со стейтом вне React - про batching нужно думать самому.
Prioritizing - важное сначала. Не всё одинаково срочно. То что пользователь видит сейчас - важнее того что за пределами экрана. requestAnimationFrame для визуальных обновлений, requestIdleCallback для всего что можно отложить.
Deferring - откладывать незрительное. Аналитика, логирование, синхронизация данных которые не нужны прямо сейчас - всё это можно отложить. Браузер сам подскажет когда у него есть свободное время.
Compositor вместо JS для анимаций. CSS свойства transform и opacity браузер анимирует на уровне GPU без участия основного потока. Если двигаешь элемент через left и top - ты грузишь поток при каждом кадре. Если через transform: translate() - нет.
/* Грузит основной поток */
.element { left: 100px; }
/* Не грузит - всё на GPU */
.element { transform: translateX(100px); }
Web Workers - тяжёлые вычисления в отдельный поток. Парсинг большого JSON, поиск по данным, криптография - если это нужно делать в браузере, Web Worker позволяет вынести это из основного потока полностью. Главная оговорка: Worker не имеет доступа к DOM, только к чистым вычислениям.
Как это выглядит в реальном проекте
На своём сайте я добавил infinite scroll - заметки подгружаются при прокрутке. Первая реализация была прямолинейной: получил данные, сразу отрендерил всё.
На небольшом количестве заметок работало нормально. Когда заметок стало больше сотни - при прокрутке начало появляться лёгкое подёргивание. Сеть тут ни при чём - данные приходили быстро. Поток просто не успевал рендерить и одновременно реагировать на скролл.
Решение простое: рендерить чанками, давать браузеру время между вставками, не делать всё за один синхронный проход.
Главная мысль
Оптимизация это не только про байты которые летят по сети. Это ещё и про то что происходит после того как они долетели.
Основной поток - самый ограниченный ресурс фронтенда. Не потому что он медленный, а потому что он один. И всё что ты заставляешь его делать - он делает вместо того чтобы реагировать на пользователя. 🖥️

Комментарии