Блокирующие ресурсы и как их убрать
Мы знаем, что CSS блокирует рендеринг, а скрипты — парсинг. Теперь научимся снимать эту блокировку так, чтобы страница не сломалась.
Блокирующий ресурс (render-blocking resource) — файл, который браузер обязан скачать и обработать до первой отрисовки. Задача оптимизации — не удалить его, а вывести из критического пути: оставить в пути минимум, необходимый для первого экрана, а остальное догрузить потом.
Зачем это делать
Отчёт Lighthouse почти на любом сайте начинается одинаково. Выглядит это примерно так:
Eliminate render-blocking resources — Potential savings: 1 180 ms
/css/main.css 186 KB 720 ms
/css/vendor/slider.css 34 KB 210 ms
https://fonts.googleapis.com/… 12 KB 250 ms
Reduce the impact of third-party code — Main-thread blocking: 1 240 ms
Полторы секунды белого экрана — не потому, что сайт «тяжёлый», а потому, что три файла стоят в очереди перед первым пикселем. Ниже — три рычага, которые эту очередь разбирают: CSS, шрифты, чужие скрипты.
Рычаг 1: критический CSS инлайном
Идея простая: стили, нужные для видимой части страницы («above the fold»), кладём прямо в <head> тегом <style>. Всё остальное — модалки, футер, слайдер, страница профиля — грузим отдельным файлом, который рендер не блокирует.
<head>
<meta charset="utf-8">
<!-- 1. Критические стили: 6–14 КБ прямо в документе. Ноль сетевых запросов. -->
<style>
:root{--bg:#fff;--fg:#111}
body{margin:0;font:16px/1.5 Inter,Arial,sans-serif;color:var(--fg);background:var(--bg)}
.header{height:64px;display:flex;align-items:center}
.hero{aspect-ratio:16/9;width:100%;background:#eee}
</style>
<!-- 2. Остальное — асинхронно. Приём с media="print": для экрана файл
не нужен, значит рендер он не блокирует; onload переключает его на all. -->
<link rel="stylesheet" href="/css/main.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
</head>
Второй вариант того же трюка — через rel="preload":
<link rel="preload" href="/css/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
Что здесь важно. Оба приёма опираются на обработчик onload, поэтому <noscript>-дублёр обязателен — иначе при отключённом JS страница останется голой. Критический CSS обычно не пишут руками, его извлекают инструментами (critical, critters, beasties, встроенные плагины Next/Nuxt) на этапе сборки, иначе он мгновенно расходится с реальными стилями. И держите его в пределах ~14 КБ: это классический размер первого окна TCP, а на практике — просто разумная граница, за которой инлайн начинает раздувать HTML.
Рычаг 2: шрифты, FOIT и FOUT
Веб-шрифт — коварный ресурс. Браузер начинает качать его только тогда, когда обнаружит, что правило @font-face реально применяется к видимому тексту, то есть уже после парсинга CSS. А до этого момента текст либо не показывается вообще, либо показывается «не тем» шрифтом.
| Термин | Что видит пользователь | Чем плохо |
| FOIT (Flash of Invisible Text) | Место под текст есть, самого текста нет | Пустой экран при живом контенте. Убивает LCP: невидимый текст не считается отрисованным |
| FOUT (Flash of Unstyled Text) | Текст сразу виден запасным шрифтом, затем перерисовывается | Мигание и — если метрики шрифтов разные — сдвиг вёрстки (CLS) |
Управляет этим свойство font-display:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900; /* вариативный шрифт: один файл на все начертания */
font-style: normal;
font-display: swap; /* показать текст сразу запасным шрифтом */
unicode-range: U+0000-00FF, U+0400-04FF; /* латиница + кириллица, лишнее не качаем */
}
auto— на усмотрение браузера, чаще всего ~3 с FOIT. Худший вариант по умолчанию.block— до 3 с невидимого текста. Только для иконочных шрифтов, где «квадратик вместо иконки» хуже пустоты.swap— рабочая лошадка: текст виден с первого кадра.fallback— 100 мс невидимости, потом запасной шрифт навсегда, если основной не успел. Компромисс без поздних перерисовок.optional— браузер сам решает; на медленной сети шрифт просто не применится в этом визите. Нулевой CLS от шрифтов, но дизайн «плавает» между визитами.
Второй шаг — не ждать парсинга CSS, а начать качать шрифт сразу:
<!-- crossorigin ОБЯЗАТЕЛЕН: шрифты качаются в анонимном CORS-режиме,
без него браузер скачает файл дважды -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
Preload имеет смысл только для 1–2 шрифтов первого экрана: обычный текст и, может быть, заголовочное начертание. Preload всех восьми файлов семейства — это не оптимизация, а конкуренция за канал с картинкой LCP.
И третье: чем ближе метрики запасного шрифта к основному, тем незаметнее подмена и тем меньше сдвиг. Это настраивается — подробно разберём в следующем уроке.
Рычаг 3: сторонние скрипты — главный тормоз
Свой код вы контролируете. Чужой — нет. При этом именно он обычно и съедает главный поток. Типичный набор на коммерческом сайте:
Third-Party Summary Transfer Size Main-Thread Blocking Time
─────────────────────────────────────────────────────────────────────────
Google Tag Manager 112 KB 430 ms
Виджет онлайн-чата 268 KB 510 ms
Счётчик веб-аналитики 64 KB 180 ms
Пиксель рекламной сети 52 KB 120 ms
A/B-тесты 41 KB 190 ms
─────────────────────────────────────────────────────────────────────────
Итого 537 KB 1 430 ms
Полторы секунды заблокированного главного потока — это провал по INP и отложенный LCP. Что с этим делать, по возрастанию усилий:
- Инвентаризация. Откройте вкладку «Третьи стороны» в Lighthouse и спросите про каждый пункт: кто владелец, какую бизнес-задачу решает, что сломается, если убрать. Половина скриптов на живых сайтах — это следы давно закрытых экспериментов.
- Всё оставшееся —
asyncилиdefer. Никаких синхронных счётчиков в<head>. - Ранние подключения. Для доменов, к которым точно пойдём:
<link rel="preconnect" href="https://analytics.example.com" crossorigin>. Экономит DNS + TLS, обычно 100–300 мс. Больше 2–4 preconnect — вредно. - Фасады (facade). Тяжёлый виджет заменяем лёгкой картинкой-заглушкой и грузим настоящий скрипт только по действию пользователя. Классика — чаты, карты, встроенные видео.
// Фасад чата: вместо 268 КБ скрипта на старте — кнопка-заглушка.
// Настоящий виджет подгружается по первому клику.
const facade = document.querySelector('#chat-facade');
function loadChat() {
const s = document.createElement('script');
s.src = 'https://widget.example.com/chat.js';
s.async = true;
document.head.appendChild(s);
facade.hidden = true; // прячем заглушку — настоящий виджет отрисуется сам
}
facade.addEventListener('click', loadChat, { once: true });
// Подстраховка: если пользователь просто сидит на странице и ничего не делает,
// догружаем виджет в простое — когда главный поток свободен.
const whenIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 4000));
whenIdle(() => {
if (!facade.hidden) loadChat();
});
Фасад — самый недооценённый приём в списке. Он не «ускоряет» скрипт, он убирает его из момента загрузки, где скорость и измеряется. По опыту, один фасад на виджет чата даёт больше, чем неделя микрооптимизаций своего бандла.
Как это работает
За кулисами всё решает приоритет запроса. Браузер раздаёт ресурсам приоритеты сам: блокирующий CSS и шрифты первого экрана — Highest, картинки во вьюпорте — High, async-скрипты и картинки за экраном — Low. Наши приёмы, по сути, — способы вручную подвинуть ресурс по этой шкале:
media="print"опускает CSS с Highest до Low — рендер он больше не держит;rel="preload"поднимает шрифт или картинку до High/Highest;fetchpriority="high"на LCP-картинке говорит браузеру, что она важнее остальных изображений;fetchpriority="low"на стороннем скрипте — наоборот, отодвигает его в конец очереди.
Проверить, что получилось, можно во вкладке Network DevTools: включите колонку Priority и посмотрите порядок. Если LCP-картинка идёт после виджета чата — вы нашли свою проблему.
Частые ошибки
- Инлайн критического CSS вручную. Через два спринта он расходится с реальными стилями, и первый экран рендерится сломанным. Только генерация на сборке.
- Забыть
<noscript>-дублёр при асинхронном CSS. При отключённом JS сайт превращается в набор строк без стилей. preloadшрифта безcrossorigin. Браузер скачивает файл дважды: один раз как preload, второй — как шрифт. Классика, которую видно только в Network.font-display: swapбез выравнивания метрик. Убрали FOIT — получили сдвиг вёрстки и просадку CLS.- «Скрипт же
async, значит бесплатный». Скачивание — да, параллельное. Но выполнение всё равно занимает главный поток и прерывает парсинг. - Preconnect ко всем доменам подряд. Каждое соединение — это TLS-рукопожатие и память. Больше нескольких — только вред.
Итоги
- Критический CSS — инлайном в
<head>, остальное — асинхронно черезmedia="print"/preload+ обязательный<noscript>. font-display: swapубирает невидимый текст;preloadсcrossorigin— 1–2 шрифта первого экрана, не больше.- Сторонние скрипты почти всегда дороже собственного кода. Порядок действий: выкинуть лишнее →
async/defer→preconnect→ фасады. - Все приёмы сводятся к одному: подвинуть ресурс по шкале приоритетов браузера. Проверяйте результат в колонке Priority во вкладке Network.