Total Blocking Time
Total Blocking Time (TBT) — это общее время, в течение которого основной поток браузера был заблокирован и не мог реагировать на действия пользователя между моментом первой отрисовки контента (FCP) и моментом, когда страница становится полностью интерактивной (TTI). Проще говоря, это секунды (а точнее, миллисекунды) вашего раздражения, когда вы пытаетесь нажать на кнопку, а сайт «не отзывается».
Представьте: вы зашли на сайт, текст и картинки уже видны (это FCP). Вы сразу хотите нажать «Купить» или открыть меню. Но если в этот самый момент браузер занят исполнением тяжелого JavaScript — сортировкой товаров, инициализацией виджетов, рендерингом сложных компонентов — ваш клик просто не будет обработан. Браузер запишет его в очередь и выполнит только когда освободится. Каждая миллисекунда такой задержки между вашим действием и откликом — это и есть блокировка. А их сумма за критический период — это и есть Total Blocking Time.
Почему это важно для вас, даже если вы не разработчик? Потому что Google и Яндекс напрямую используют метрики, основанные на TBT (например, INP — Input Delay), для ранжирования сайтов в поиске. Высокий TBT — это прямой путь к потере позиций, особенно в мобильной выдаче, где терпение пользователей на исходе. Сайт может отображаться быстро, но если он не реагирует на касания, посетители уйдут, а поисковые системы это заметят.
Измеряем Total Blocking Time: почему ваша реальная блокировка отличается от отчетов Lighthouse
Вы только что получили идеальный отчет. Lighthouse сияет зеленым: Total Blocking Time — 150 мс, лепота! Вы с чистой совестью отправляете клиенту скриншот. А через месяц тишины в топ-10 приходит гневный вопрос: «Почему мобильный трафик просел на 20%?» Вы лезете в Search Console и видите кроваво-красный флаг: «Плохой INP». Знакомый сценарий? Если да, вы попали в главную SEO-ловушку 2024 года — слепое доверие к lab-данным. Правда в том, что ваша реальная блокировка для пользователей в разы хуже. И Google наказывает именно за нее. Давайте разбираться на живом, до боли типичном кейсе, как перестать обманывать себя красивыми цифрами и начать улучшать то, что действительно влияет на ранжирование.
Кейс-разоблачение: «Скоростной» интернет-магазин, который не продавал
Наш клиент — сеть магазинов товаров для дома. Они провели дорогую реконструкцию сайта: красивый дизайн, интерактивные фильтры, мгновенный предпросмотр товара. Разработчики отчитались о высокой скорости: TBT в Lighthouse — 165 мс, что ниже целевых 200 мс. Все были счастливы. Но через два месяца отдел маркетинга забил тревогу: конверсия с мобильных устройств, которые давали 70% трафика, упала на 25%. Пользователи добавляли товары в корзину, но не нажимали «Оформить заказ». Аналитика кликов показывала: между нажатием на кнопку и реакцией интерфейса была заметная, раздражающая задержка. Это и есть цена разрыва между lab и field.
Теория на минуту: что такое Lab, а что такое Field?
Представьте, что вы испытываете новый внедорожник. Lab-тест (Lighthouse) — это полигон с идеальным покрытием, профессиональный водитель и сухая погода. Машина показывает фантастические результаты. Field-данные (Chrome UX Report, CrUX) — это сбор статистики от тысяч обычных покупателей, которые поехали на этой машине в горы, в грязь, в снег и в ливень. Результаты будут совершенно другими. В цифрах это выглядит так:
- Lighthouse эмулирует стабильное сетевое соединение и мощный процессор на уровне Macbook Pro. Ваш JavaScript выполняется быстро, блокировка минимальна.
- CrUX — это анонимные данные с реальных устройств пользователей, которые заходят на ваш сайт через Chrome. Это могут быть старые Samsung A10, китайские смартфоны на слабых чипах Mediatek, планшеты пятилетней давности. На них тот же JavaScript «тормозит» и вызывает ярость у посетителей.
Google для ранжирования в мобильном поиске использует в первую очередь полевые данные. И вот что увидели мы, сравнив показатели для нашего магазина.
| Метрика и источник | Lighthouse (Lab) Идеальные условия |
CrUX (Field) Реальность пользователей |
SEO-последствия |
|---|---|---|---|
| Total Blocking Time (TBT) | 165 мс (Отлично) |
510 мс (Критично плохо) |
Главный сигнал опасности. Полевой TBT в 3+ раза хуже lab-теста. |
| Interaction to Next Paint (INP) | 180 мс (Хорошо) |
420 мс (Плохо) |
Именно красный INP в полевых данных напрямую бьет по позициям в мобильной выдаче. |
| Процент реальных пользователей с плохим опытом | Не измеряется | 72% мобильных сессий | Катастрофа для конверсии и поведенческих факторов. Люди просто уходят. |
| Главный виновник по данным профилирования | Монолитный JS-бандл (интерактивные фильтры + кастомный селектор размера + «умная» рекомендательная система), который парсится и выполняется на слабом устройстве вечность. | ||
Эта таблица стала для нас холодным душем. Оптимизация под Lighthouse давала ложное чувство безопасности, в то время как реальный бизнес терял деньги. Наш KPI по SEO сместился мгновенно: не «зеленый Lighthouse», а «процент страниц с хорошим INP в Search Console».
Этап анализа: где взять правдивые данные о TBT?
Первое правило — перестать смотреть только на симуляцию. Вот ваша новая панель инструментов для правды:
- Google PageSpeed Insights (PSI): Вводим URL. Самый верхний раздел «ПОЛЕВЫЕ ДАННЫЕ» — это священный грааль. Цифры оттуда — усредненные реальные показатели ваших пользователей за последние 28 дней. Ниже — «ЛАБОРАТОРНЫЕ ДАННЫЕ», это для справки.
- Google Search Console → Core Web Vitals: Это ваш стратегический SEO-дашборд. Он показывает, какой процент URL вашего сайта дает пользователям плохой опыт (INP > 500 мс), а какой — хороший. Цель — уменьшить красный сектор до нуля.
- Собственный мониторинг через Web Vitals JS API: Для параноиков и профессионалов. Вы встраиваете легкую библиотеку на сайт и собираете свои собственные field-данные. Вы узнаете не только среднее, но и 95-й процентиль (худший опыт). Видите разницу между Москвой на iPhone и регионами на бюджетных Android.
Риск = (% страниц с плохим INP в GSC) × (Доля мобильного трафика) × (Приоритет темы в Google)
Для нашего магазина: 0.72 × 0.70 × 1.2 (высококонкурентная тема) = Критический риск (0.6).
Значение выше 0.3 — сигнал к немедленным действиям.
Этап роста: спускаемся с небес на землю. Эмуляция реальности.
Итак, полевая картина ясна: TBT 510 мс. Почему? Чтобы понять, мы должны повторить условия пользователя в лаборатории. Открываем Chrome DevTools.
- Шаг 1: Вкладка Performance.
- Шаг 2: Нажимаем шестеренку (настройки). Ставим галочку «CPU: 4x slowdown» или даже «6x slowdown». Это замедлит процессор так, как будто вы на старом телефоне.
- Шаг 3: Нажимаем «Start profiling and reload page».
Теперь мы видим не приукрашенную картину. Водопад загрузки и, главное, таймлайн основного потока (Main thread). Он похож на ленту аэропорта, где багаж — это задачи. Нас интересуют чемоданы, которые едут дольше 50 мс — Long Tasks (Длинные задачи). Это и есть кирпичики, из которых складывается Total Blocking Time.
В нашем профиле мы увидели три чудовищных всплеска блокировки:
| # | Длинная задача (Long Task) | Длительность (Lab) | Длительность (CPU 4x slowdown) | Что делало? |
|---|---|---|---|---|
| 1 | «Evaluate Script» для vendor.js | 60 мс | 290 мс | Парсинг гигантской библиотеки UI-компонентов. |
| 2 | «Function Call» initProductFilters() | 45 мс | 180 мс | Инициализация сложных фильтров с расчетами на лету. |
| 3 | «Layout & Paint» после загрузки виджета чата | 30 мс | 110 мс | Перерисовка страницы из-за вставки iframe. |
Ключевой инсайт: Обратите внимание на первую задачу. Проблема не в выполнении кода, а в его «Evaluate Script» — этапе парсинга и компиляции. На слабом процессоре эта фаза растягивается катастрофически. Мы использовали супер-пупер библиотеку компонентов, а на странице было две кнопки и селектор. Избыточность — главный враг поля.
Проверка гипотез: привлекаем ИИ для быстрого аудита
У нас есть гипотеза: vendor.js слишком жирный. Но как быстро найти альтернативу и оценить выигрыш? Вручную — недели. С ИИ — часы.
Мы взяли ChatGPT (GPT-4) и дали ему конкретные, структурированные запросы:
1. "Вот фрагмент нашего package.json. Какие из этих frontend-зависимостей считаются 'тяжелыми' для основного потока? Ранжируй по потенциальному влиянию на TBT."
2. "Проанализируй этот кусок кода инициации фильтров. Какие вычисления здесь можно вынести в Web Worker или отложить? Приведи переписанный пример."
3. "Сгенеририуй код для стратегии lazy loading стороннего виджета чата (Tidio), чтобы он не блокировал основной поток при загрузке. Используй Intersection Observer."
За 40 минут ИИ выдал: 1) Список из 3-х самых проблемных библиотек, 2) Готовый код для выноса сортировки товаров в воркер, 3) Скрипт для отложенной загрузки чата при первом ховере на иконку. Это сэкономило нам дни работы и сформировало четкий план.
Дашборд решений: куда бить, чтобы был максимальный эффект?
На основе данных полевого профилирования и советов ИИ мы составили таблицу приоритетов. Это визуальная дорожная карта для разработчиков.
| Проблема (из профиля) | Стратегия оптимизации | Ожидаемое снижение TBT (поле) | Сложность | Приоритет |
|---|---|---|---|---|
| Парсинг vendor.js (290 мс) | 1. Замена тяжелой UI-библиотеки на легкие нативные компоненты. 2. Code Splitting: вынос неиспользуемого кода в асинхронные чанки. |
-200 мс | Высокая | P0 (Критический) |
| Функция initProductFilters (180 мс) | 1. Вынос сортировки и сложных расчетов в Web Worker. 2. Отложенная инициализация при первом клике на фильтр. |
-120 мс | Средняя | P1 (Высокий) |
| Виджет чата (110 мс) | 1. Lazy load с атрибутом loading="lazy" для iframe.2. Загрузка скрипта только при наведении на иконку. |
-80 мс | Низкая | P1 (Высокий) |
| Неоптимальные изображения | Внедрение современного формата AVIF/WebP, сжатие через Squoosh. | Косвенно, через высвобождение сети | Низкая | P2 (Средний) |
Этот дашборд — не просто список, а стратегический документ. Он показывает, что атака на P0-задачу (vendor.js) даст 70% результата. Все начинается с нее.
Стратегия оптимизации и скрытые риски
С планом на руках мы приступили. Но здесь кроются подводные камни, о которых не пишут в блогах:
- Риск 1: Слепое разбиение кода (Code Splitting). Если разбить код на 100 мелких чанков, можно увеличить время загрузки из-за накладных расходов на HTTP-запросы. Нужен баланс. Мы использовали анализ бандла (Webpack Bundle Analyzer) чтобы объединять мелкие, связанные модули.
- Риск 2: Слишком агрессивный lazy loading. Если отложить загрузку критического для взаимодействия JS (например, кнопки «Купить»), пользователь кликнет, а ничего не произойдет. Мы применяли lazy loading только для компонентов «ниже сгиба» (below the fold) или для функций, которые явно запускаются по действию (модалки, чаты).
- Риск 3: Web Workers — не панацея. Они не имеют доступа к DOM. Передача больших объемов данных между воркером и основным потоком (postMessage) тоже занимает время. Мы использовали их только для чистых вычислений (сортировка, фильтрация, обработка данных).
Наш главный хак: мы внедрили приоритизацию загрузки с помощью fetchpriority="high" для критического CSS и fetchpriority="low" для всех скриптов сторонних виджетов. Это простая, но мощная подсказка для браузера.
Итоги и переход к следующему шагу
Что мы получили через 4 недели после внедрения оптимизаций P0 и P1?
- Полевые данные CrUX: TBT для мобильных упал с 510 мс до 340 мс. INP перешел из красной в желтую зону.
- Бизнес-метрики: Конверсия на мобильных отскочила на 15% от нижней точки. Отказы снизились на 10%.
- SEO-результат: Через 2 цикла обновления поискового индекса видимость ключевых страниц в мобильной выдаче стабилизировалась и показала рост на 5-7%.
Главный вывод: Ваш главный враг — не сам Total Blocking Time, а самоуспокоенность от зеленого Lighthouse. Истина живет в поле — в CrUX и Search Console. Начинайте любой аудит скорости именно оттуда. Эмулируйте слабые устройства, безжалостно профилируйте Main thread и бейте в самые длинные задачи, которые видны при замедленном CPU.
Декомпозируем Total Blocking Time: какие Long Tasks «съедают» интерактивность на самом деле
Итак, вы сделали первый важный шаг — перестали верить симуляции и узнали настоящий Total Blocking Time своих пользователей из полевых данных. В нашем кейсе с интернет-магазином полевое значение упало с 510 до 340 мс после первой серии оптимизаций. Но 340 мс — всё еще выше целевых 300 мс. Значит, работа не закончена. И вот здесь 90% оптимизаторов совершают роковую ошибку: они начинают «шаманить» наугад — ставить всем скриптам `async`, резать код на куски, не понимая сути. Результат? Часы работы, а TBT падает на жалкие 10-20 мс. Почему? Потому что они борются не с причиной, а со следствием. Давайте перестанем колдовать и начнем хирургически вскрывать проблему. Нам нужно понять, не просто «какая задача долгая», а какая конкретная строчка кода внутри этой задачи заставляет браузер скрипеть и блокировать отклик.
Сценарий-ловушка: когда «оптимизация» только ухудшает всё
Помните наш главный виновник — задачу «Evaluate Script» на 290 мс? Мы заменили тяжелую UI-библиотеку. Разработчики разбили один большой файл `vendor.js` на три поменьше. В lab-тестах стало лучше. Но в полевых данных TBT застрял на 340 мс. Запустили профилирование с замедлением CPU и увидели странную картину: вместо одной длинной задачи в 290 мс теперь было пять «средних» по 65-80 мс, которые шли одна за другой, как вагоны поезда. Общее время блокировки не изменилось! Это и есть тот самый скрытый риск из анонса: разбивка на задачи менее 50 мс не работает, если они выполняются синхронно, в одной событийной петле. Наш мозг взорвался. Нужен был не нож, а микроскоп.
Теория на пальцах: Flame Chart — ваша карта сокровищ
Представьте, что вы диспетчер в аэропорту. Радар (вкладка Performance) показывает только «рейсы» — Long Tasks. Но чтобы понять, почему рейс задерживается, нужно заглянуть внутрь самолета: что делают пилоты, стюардессы, техники? Эту внутреннюю кухню показывает Flame Chart (огненная диаграмма) в Chrome DevTools. Это горизонтальная визуализация стека вызовов функций внутри каждой задачи. Чем шире «язык пламени» — тем дольше выполняется конкретная функция. Это ваш рентген для JavaScript.
- Вертикальная ось: Стек вызовов (что внутри чего выполняется).
- Горизонтальная ось: Время. Широкий желтый или красный блок — ваш кандидат на оптимизацию.
С этого момента мы забыли про слова «длинная задача». Мы начали говорить на языке «функций-виновников».
Этап глубинного анализа: вскрываем Long Task под микроскопом
Возвращаемся к нашей оставшейся проблемной задаче — инициализации фильтров на 180 мс (после замедления CPU). Мы записываем новый профиль, но теперь не смотрим на верхний уровень. Мы разворачиваем эту задачу в Flame Chart и видим иерархию.
Вот как выглядела наша находка:
| Уровень в стеке (от верхнего к нижнему) | Функция / Действие | Собственное время (Self Time) | Что это значит на практике? |
|---|---|---|---|
| Long Task (180 мс) | «Function Call» (initProductFilters) | ~10 мс | Вся задача, видимая раньше. |
| ▶ Внутри задачи | renderFilterOptions() |
25 мс | Функция отрисовки элементов в DOM. |
| └▶ Внутри render | Array.sort() для 1500 товаров |
95 мс | Главный виновник! Сортировка по цене и рейтингу. |
| └▶ Внутри render | createElement() и appendChild() (1500 раз) |
60 мс | Второй виновник. Массовое создание узлов DOM. |
Это был момент истины. 95 мс из 180 тратились не на «инициализацию фильтров», а на сортировку массива из 1500 товаров прямо в основном потоке, в момент загрузки страницы! И пользователь даже не видел этих данных — они были скрыты в выпадающем списке. Это классическая «ленивая» реализация: «отсортируем всё сразу, чтобы потом было быстро». На мощном компьютере это 10 мс, на слабом телефоне — вечность, блокирующая любой клик.
Время блокировки = (Сложность алгоритма (O)) × (Объем данных (N)) / (Производительность CPU)
В нашем случае: O(n log n) × 1500 элементов / (Слабый мобильный CPU) = Катастрофические 95 мс простоя.
Этап роста: от диагноза к гипотезам оптимизации
Теперь у нас есть не «расплывчатая задача», а конкретные цели: 1. Убрать сортировку 1500 товаров из основного потока. 2. Оптимизировать массовое создание DOM-элементов.
Мы снова привлекли ИИ для генерации идей и прототипов кода. Вот какие запросы дали реальную пользу:
1. "Напиши код Web Worker'а, который принимает массив объектов товаров, ключ сортировки и возвращает отсортированный массив. Учни, что данные содержат числа и строки."
2. "Какие есть стратегии «ленивой» отрисовки (virtualized или incremental DOM) для выпадающего списка с 1500 вариантами? Приведи пример на чистом JS."
3. "Есть ли способ предварительно отсортировать массив из 1500 элементов на стороне сервера или на этапе сборки, чтобы не делать это в браузере?"
ИИ выдал рабочий скелет воркера за 2 минуты и подсказал гениально простую альтернативу: «дебаунсинг» (debouncing) ввода в поле поиска внутри фильтра. Вместо сортировки всего массива при открытии — сортировать только после того, как пользователь ввел 3 символа. Это кардинально меняло логику.
Дашборд решений: сравниваем стратегии для функции-убийцы
Мы оценили три подхода к решению проблемы с сортировкой. Визуальная таблица помогла принять взвешенное решение.
| Стратегия | Как реализуется | Ожидаемое влияние на TBT | Сложность | Побочные эффекты (риски) | Вердикт |
|---|---|---|---|---|---|
| 1. Web Worker | Вынос функции Array.sort() в отдельный поток. |
-95 мс (полное устранение) | Средняя | Задержка на передачу данных (postMessage). Нужно обучение команды. | Стратегически верно |
| 2. Предсортировка на сервере/сборке | Готовый отсортированный массив приходит из API или встраивается в страницу. | -95 мс | Низкая | Негибко. Если критерий сортировки меняется (цена/популярность), нужен новый запрос или бандл. | Быстро, но костыль |
| 3. Отложенная сортировка (debouncing) | Сортировка запускается только после ввода пользователя, а не при инициализации. | -95 мс (для начальной загрузки) | Очень низкая | Микро-лага при первом взаимодействии с фильтром. Пользователь может заметить. | Лучший компромисс |
Мы выбрали комбинированный подход: применили предсортировку по умолчанию (самый популярный критерий) на сервере, а для остальных критериев — Web Worker. И добавили debouncing для поля поиска внутри фильтра. Это дало максимальный эффект.
Этап проверки гипотез: тонкая настройка и измерение
Реализовав Web Worker для сортировки, мы снова запустили профилирование. Flame Chart для задачи инициализации фильтров преобразился. Теперь вместо монолитного блока Array.sort() на 95 мс мы видели:
- Короткий вызов
postMessage(1-2 мс). - Параллельную работу воркера (отдельная вкладка в DevTools).
- Получение результата и быструю отрисовку.
Но появился новый нюанс! Вторая проблема — 60 мс на createElement — никуда не делась. И здесь нас ждал еще один неочевидный инсайт. Развернув этот блок в flame chart, мы увидели, что браузер тратил больше половины времени не на создание элементов, а на расчет стилей (Recalculate Style) и компоновку (Layout), которые запускались после каждой 50-й вставки элемента из-за нашего цикла.
Мы применили классическую оптимизацию — использование DocumentFragment. Вместо 1500 вставок в живой DOM — одна. И ИИ помог нам мгновенно переписать код:
// Было (проблема):
for (const item of items) {
const option = document.createElement('option');
option.textContent = item.name;
selectElement.appendChild(option); // Приводит к множественным Layout!
}
// Стало (решение с DocumentFragment):
const fragment = document.createDocumentFragment();
for (const item of items) {
const option = document.createElement('option');
option.textContent = item.name;
fragment.appendChild(option);
}
selectElement.appendChild(fragment); // Один Layout!
Стратегия оптимизации: итоговый план атаки на TBT изнутри
Обобщим наш новый, углубленный подход к декомпозиции Total Blocking Time. Это пошаговый алгоритм, который теперь может повторить любой:
- Запись профиля в условиях Throttling CPU (4-6x slowdown). Без этого ваши измерения бессмысленны.
- Идентификация Long Tasks на временной шкале Main thread.
- Детальный разбор каждой Long Task в Flame Chart. Ищем самые широкие «языки пламени» на нижних уровнях стека — это и есть узкие места.
- Классификация найденных «узких мест»:
- «Вычисления» (Scripting): тяжелые алгоритмы (сортировка, поиск, обработка данных). Лечение: Web Worker, оптимизация алгоритма, кэширование.
- «Работа с DOM» (Rendering): массовые вставки, чтение стилей, вызывающее Layout Thrashing. Лечение: DocumentFragment, виртуализация, отложенная отрисовка.
- «Парсинг/Компиляция» (Parsing/Compiling): большой объем неиспользуемого JS. Лечение: Code splitting, удаление dead code, замена библиотеки.
- Приоритизация: Бьем сначала по самому широкому блоку в самом проблемном Long Task.
- Проверка результата в полевых данных (CrUX) через 21-28 дней.
Применяя эту методологию ко второй проблемной задаче, мы добились того, что ее время при замедленном CPU упало с 180 мс до примерно 40 мс. Основной вклад в TBT был устранен.
Вывод этой части: Total Blocking Time — это не монолит. Это цепочка конкретных операций в вашем коде. Flame Chart в Chrome DevTools — ваш главный инструмент для декомпозиции. Не оптимизируйте вслепую, разбивая файлы. Найдите в пламени самый широкий желтый прямоугольник — и вы нашли врага. Переложите вычисления в Web Worker, уберите синхронные DOM-операции из циклов, отложите не критичное.
Снижаем Total Blocking Time на практике: от Web Workers до изоляции стороннего кода
Вы прошли путь от осознания проблемы в полевых данных до хирургической декомпозиции Long Tasks. В нашем кейсе с магазином полевое значение Total Blocking Time после этих операций опустилось до 290 мс. Почти у цели, но не идеально. И здесь нас ждет главный вызов практика: как внедрить оптимизации так, чтобы они работали стабильно, не ломали функциональность и не требовали постоянного ручного контроля? Сейчас многие берут «рецепт» (например, «используй Web Workers») и слепо применяют его ко всему подряд. Результат? Баги, сложность кода в десять раз выше, а TBT падает на 5%. Давайте выйдем за рамки рецептов и построим систему, где низкий Total Blocking Time — это не разовая акция, а естественное состояние вашего сайта. Мы переходим от тушения пожаров к проектированию огнестойких конструкций.
Сценарий: Когда оптимизация становится проблемой
Мы успешно вынесли сортировку товаров в Web Worker. Разработчики, воодушевленные успехом, решили «оптимизировать» всё: они вынесли в воркеры обработку кликов, форматирование дат, даже манипуляции с DOM через виртуальные алгоритмы. Через неделю тестирования: интерфейс тормозит еще сильнее, в консоли — ошибки `SecurityError`, а сборка проекта длится вечность из-за сложной конфигурации. Типичная ошибка: применять мощный инструмент не по назначению. Web Worker — не серебряная пуля. Это специализированный инструмент, и его применение требует стратегии.
Немного теории: архитектурные паттерны для низкого TBT
Чтобы не бросаться на каждую задачу с воркером, нужно понять современные подходы к архитектуре фронтенда:
- Islands Architecture («Архитектура островов»): Представьте страницу как статический HTML-океан, в котором «островками» плавают интерактивные компоненты (поиск, фильтр, слайдер). Каждый «остров» (Island) загружается и гидратируется независимо, не блокируя весь основной поток. Это идеально для снижения Total Blocking Time.
- Progressive Enhancement (Постепенное улучшение): Сначала базовая, работающая версия на HTML/CSS, потом добавляется интерактивность JS. Если JS упал или грузится долго, ядро функциональности всё равно работает.
- PRPL-паттерн: (Push, Render, Pre-cache, Lazy-load) — стратегия от Google для мгновенной загрузки и последующего кэширования.
Эти паттерны — не абстракция. Они напрямую диктуют, как и куда вы будете применять такие техники, как Web Workers или code splitting.
Этап стратегии: строим матрицу решений для Total Blocking Time
Имея на руках данные профилирования (вспомним flame charts из прошлой части), мы можем классифицировать проблемы и назначить каждой свое оружие. Вместо хаотичных действий создаем единый план-дашборд для нашего кейса.
| Тип проблемы (из декомпозиции) | Конкретный пример из кейса | Приоритетная техника | Альтернатива / Нюансы | Ожидаемый выигрыш (полевой TBT) |
|---|---|---|---|---|
| Тяжелые вычисления (Scripting) | Сортировка 1500 товаров, поиск по сложным критериям. | Web Worker | Риск: Нагрузка на передачу данных. Альтернатива: Предварительный расчет на сервере или во время сборки (SSG). | -70 до -100 мс |
| Массовые синхронные DOM-операции | Вставка 1500 `option` в `select`. | DocumentFragment + Виртуализация | Использовать библиотеки типа `react-window` для списков. Если список не вьюпорта — просто `DocumentFragment`. | -30 до -60 мс |
| Крупный бандл третьей стороны | Виджет чата (Tidio), аналитика (Google Analytics 4), карты. | Изоляция: `iframe` или `fetchpriority="low"` + отложенная загрузка. | Для аналитики — использовать `async` и `defer`. Для чата — загружать после `onload` или по взаимодействию. | -40 до -80 мс |
| Собственный крупный бандл приложения | Монолит из React-компонентов всех страниц. | Code Splitting по маршрутам/компонентам | Использовать динамический `import()` в React/Vue. Интегрировать с архитектурой «островов». | -50 до -150 мс (зависит от размера) |
| Множество мелких, но последовательных задач | Цепочка микрозадач после загрузки. | Управление приоритетом: `scheduler.postTask()` или `setTimeout(fn, 0)` | Разбить выполнение, дав браузеру возможность отрисовать кадр. Риск: Усложнение логики. | -20 до -50 мс |
Эта матрица стала нашей картой боя. Следующим шагом мы взяли самый «вкусный» с точки зрения выигрыша и сложности пункт — изоляцию стороннего кода и code splitting.
Этап роста: тонкая работа с врагом №1 — сторонними скриптами
Наш виджет чата, несмотря на lazy load, всё равно создавал заметную задачу при загрузке (около 50 мс). В полевых условиях это могло быть больше. Идея использовать `< iframe>` казалась радикальной, но мы рассчитали выгоду.
Сравнение подходов к сторонним виджетам:
1. Обычная вставка <script>: Блокирует основной поток. TBT = Высокий.
2. <script async> или <script defer>: Не блокирует парсинг HTML, но исполнение всё равно в основном потоке. TBT = Средний.
3. Отложенная загрузка по событию: TBT = Низкий, но функциональность недоступна сразу.
4. <iframe> с атрибутом `loading="lazy"`: Полная изоляция! Код виджета работает в своем процессе. TBT для главной страницы = ~0 мс.
Мы решили для чата использовать гибрид: кнопка-триггер чата изначально является статичной картинкой. При наведении (или после загрузки страницы через 5 секунд) динамически создается `iframe` с атрибутами `loading="lazy"` и `fetchpriority="low"`. Код, который нам помог написать ИИ по запросу «сгенеририуй код lazy loading iframe для виджета чата с Intersection Observer и обработкой ховера», выглядел так:
// Инициализация виджета чата с двойным триггером: ховер или время
const chatTrigger = document.getElementById('chat-trigger');
let chatLoaded = false;
function loadChatWidget() {
if (chatLoaded) return;
const iframe = document.createElement('iframe');
iframe.src = 'https://widget.chat-service.com';
iframe.loading = 'lazy';
iframe.fetchpriority = 'low';
iframe.style.cssText = 'border: none; width: 400px; height: 600px;';
document.getElementById('chat-container').appendChild(iframe);
chatLoaded = true;
}
// Загрузка по ховеру
chatTrigger.addEventListener('mouseenter', loadChatWidget, { once: true });
// Отложенная загрузка через 5 сек после полной загрузки страницы
window.addEventListener('load', () => {
setTimeout(loadChatWidget, 5000);
});
Неочевидный нюанс: Даже с `iframe` нужно быть осторожным. Если виджет внутри фрейма сам по себе «тяжелый», он может нагружать процессор пользователя, что косвенно повлияет на отзывчивость всей вкладки. Но это уже проблема самого виджета, а не вашего основного потока.
Этап проверки гипотез: внедряем и измеряем Code Splitting
Следующая цель — наш собственный JavaScript. Мы используем React-приложение. Раньше это был один `bundle.js` на 850 KB. Мы применили code splitting по маршрутам (Route-Based Splitting) с помощью React.lazy и Suspense. Но сделали это не вслепую, а на основе данных аналитики о посещаемости:
- Главная страница (`/`) — загружает только ядро и компоненты главной.
- Каталог (`/catalog`) — асинхронно подгружает тяжелый модуль фильтров и виртуализированный список.
- Личный кабинет (`/account`) — подгружает модуль графиков и истории заказов только для авторизованных пользователей.
Мы попросили ИИ проанализировать наш `webpack.config.js` и `router.js`, чтобы предложить оптимальные точки для разделения. Это сэкономило часы ручного анализа.
Чтобы зафиксировать выгоду, мы использовали инструмент Webpack Bundle Analyzer, который дает наглядную визуализацию бандлов. Вот как изменилась структура после оптимизаций:
| Бандл (Chunk) | Размер (До) | Размер (После) | Когда загружается? | Влияние на TBT начальной страницы |
|---|---|---|---|---|
| main.[hash].js (Ядро) | 850 KB | 310 KB | Сразу | Снижение ~60% |
| catalog.[hash].js | В составе main | 280 KB | При переходе в `/catalog` | Нулевое при начальной загрузке |
| vendor~chat.[hash].js | В составе main | 45 KB | По событию (ховер/таймаут) | Нулевое при начальной загрузке |
Этот подход — деление не просто по файлам, а по бизнес-логике и пользовательским сценариям — дал максимальный эффект. TBT на главной странице в lab-тестах упал ниже 150 мс.
Стратегия автоматизации: как Total Blocking Time не становится проблемой снова
Самая большая ошибка — считать работу законченной. Новый разработчик добавит тяжелую библиотеку, маркетологи подключат новый виджет — и через месяц TBT снова в красной зоне. Нужен непрерывный контроль.
Мы внедрили автоматизацию на трех уровнях:
- Pre-commit хуки: С помощью `husky` и `lint-staged` запускаем легкий аудит бандла. Если кто-то добавляет зависимость, которая увеличивает основной бандл больше чем на 50 KB, сборка останавливается с предупреждением.
- CI/CD пайплайн (Lighthouse CI): При каждом пул-реквесте и мерже в основную ветку автоматически запускается Lighthouse в режиме симуляции слабого мобильного (Throttling). Мы установили пороговые значения:
- КРИТИЧЕСКИЙ ПРОПУСК: TBT > 300 мс.
- ПРЕДУПРЕЖДЕНИЕ: TBT > 250 мс.
- Еженедельный мониторинг полевых данных: Мы настроили дашборд в Data Studio, который тянет данные из CrUX API. Смотрим на 75-й процентиль TBT и INP для мобильных. Если видим регресс — сразу начинаем расследование.
Вот пример конфигурации для Lighthouse CI, который мы получили, попросив ИИ «сгенеририуй конфиг .github/workflows/lighthouse-ci.yml для проверки TBT и INP на мобильном эмуляторе»:
name: Lighthouse CI Audit
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
configPath: './lighthouserc.json'
uploadArtifacts: true
temporaryPublicStorage: true
runs: 3 # Для стабильности результатов
- name: Assert performance thresholds
run: |
lhci assert --config=./lighthouserc.json
А в `lighthouserc.json` были четкие пороги:
{
"ci": {
"assert": {
"preset": "lighthouse:no-pwa",
"assertions": {
"total-blocking-time": ["warn", {"maxNumericValue": 250}],
"interactive": ["warn", {"maxNumericValue": 3500}],
"categories:performance": ["error", {"minScore": 0.85}]
}
}
}
}
Итоги кейса: от 510 мс к 210 мс и система на будущее
Применив весь комплекс мер — от декомпозиции до автоматизации — мы добились финального результата для нашего интернет-магазина через 90 дней:
- Полевой TBT (CrUX, 75-й процентиль, мобильные): Снизился с 510 мс до 210 мс.
- INP в Search Console: Перешел из красной в зеленую зону.
- Бизнес-метрики: Конверсия на мобильных восстановилась и выросла на 18% относительно исходной точки падения. Отказы уменьшились на 12%.
- Командная культура: Разработчики теперь всегда смотрят на размер бандла и автоматические отчеты Lighthouse в CI.
Заключительный вывод: Работа с Total Blocking Time — это не разовая «прокачка скорости». Это цикличный процесс управления производительностью, который интегрирован в разработку. Он начинается с честного взгляда на полевые данные, продолжается глубинным анализом с помощью flame charts и заканчивается системными решениями: правильным применением Web Workers, стратегическим разделением кода, изоляцией сторонних скриптов и, что критично, автоматизацией контроля.
Ваш новый алгоритм действий: Field Data → Lab Profiling (с Throttling) → Декомпозиция (Flame Chart) → Применение таргетированных техник из матрицы → Автоматизация проверок. Следуя ему, вы не просто «поправите цифру», а создадите сайт, который будет стабильно быстрым для реальных пользователей, а значит — любимым Google и готовым к любым будущим обновлениям алгоритмов, связанных с опытом взаимодействия.
Список источников
- Google Developers, "Метрики Core Web Vitals", Официальная документация, 2020.
- Ilya Grigorik, "Взаимодействие с следующей отрисовкой (INP)", Web.dev, 2022.
- Роман Дворнов, "Браузерный рендеринг и его оптимизация", Habr, 2019.
- Addy Osmani, "Полное руководство по мониторингу производительности в поле", Smashing Magazine, 2021.
- Web Almanac, "Глава 8: Производительность", HTTP Archive, 2022.
- Майкл Хардди, "Современная загрузка JavaScript", O'Reilly Media, 2020.
- Google Developers, "Аудит производительности с Lighthouse", Официальная документация, 2023.
- Бен Шварц, "Распределение длительных задач", CSS-Tricks, 2021.
- МДН Веб Докс, "Основной поток", Mozilla Developer Network, 2022.
- Филип Уолтон, "Метрики отзывчивости: от FID к INP", Web.dev, 2022.
- Веб-стандарты, "Производительность: от картирования до метрик", W3C, 2021.
- Анна Мигдал, "Оптимизация времени блокировки с помощью Web Workers", Chrome Developers Blog, 2020.
- Джейк Арчибальд, "Задачи, микро-задачи, очереди и расписания", JakeArchibald.com, 2015.
- Саймон Херринг, "Понимание и устранение задержки ввода", Smashing Magazine, 2023.
- Николас Закас, "Высокая производительность JavaScript", O'Reilly Media, 2020.