Критический CSS и отложенная загрузка стилей без FOUC
Как выделить критический CSS для первого экрана, отложить остальные стили без мигания незастилованного контента и не сломать рендеринг на медленных соединениях.

Задача и почему прямой путь даёт проблему
Один тег <link rel="stylesheet"> в <head> блокирует рендеринг: браузер не покажет ни строки, пока не скачает и не распарсит весь файл. На проекте с бандлом стилей в 180 КБ это добавляет к First Contentful Paint 300–600 мс на медленном 4G — Lighthouse отмечает это правилом «Eliminate render-blocking resources».
Очевидное решение — навесить на весь <link> отложенную загрузку через media="print" и переключение на all в onload. Проблема: пока файл грузится, страница рисуется вообще без стилей. Пользователь видит FOUC1 — прыгающий нестилизованный текст. На мобильном это выглядит как сломанный сайт.
Правильный путь — разделить CSS на две части: критический (стили для того, что видно на первом экране) встраивается инлайном в <head>, остальное грузится отложенно. Ниже — как выделить критику, как отложить остаток без мигания и где всё это ломается.
1 FOUC — Flash of Unstyled Content, кратковременный показ страницы без применённых стилей.
Что считать критическим CSS
Критический CSS — это минимальный набор правил, нужный для отрисовки контента выше линии сгиба (above the fold). Не «важные стили», а именно те, что применяются к элементам в видимой при загрузке области.
Определять «на глаз» бессмысленно: селекторы каскадируются, и вручную вы пропустите половину. Извлечение автоматизируют инструменты, которые запускают безголовый браузер, рендерят страницу в заданном вьюпорте и собирают применённые правила. Рабочие варианты на 2024–2025:
- critical (npm-пакет от Addy Osmani) — обёртка над Penthouse и Puppeteer, встраивает критику прямо в HTML;
- penthouse — низкоуровневый экстрактор, если нужен только CSS без вставки в разметку;
- beasties (форк заброшенного critters) — встраивает критический CSS на этапе сборки, работает с уже собранным HTML без запуска браузера, за счёт этого быстрее, но менее точен на динамике.
Извлечение через critical
Пакет critical версии 7.x. Установка и минимальный вызов из Node:
// npm install critical --save-dev
// Node 18+, critical 7.x
import { generate } from 'critical';
await generate({
// исходная страница: URL или локальный файл
src: 'https://example.com/',
// ширина и высота вьюпорта для расчёта первого экрана
width: 1300,
height: 900,
// вернуть только строку критического CSS
inline: false,
// минифицировать результат
minify: true,
target: {
css: 'dist/critical.css',
},
});
Проверка: файл dist/critical.css должен весить единицы килобайт, обычно 8–15 КБ. Если он вышел на 100 КБ — экстрактор захватил лишнее (например, стили модалок или футера), и настройки вьюпорта или разметку above-the-fold стоит пересмотреть.
Несколько вьюпортов
Первый экран на десктопе и на телефоне — разный контент и разные медиазапросы. Одного вьюпорта мало. critical принимает массив размеров:
await generate({
src: 'https://example.com/',
// критика для мобильного и десктопного первого экрана
dimensions: [
{ width: 375, height: 812 },
{ width: 1300, height: 900 },
],
minify: true,
target: { css: 'dist/critical.css' },
});
Как встроить критику и отложить остальное
Схема в <head>: сначала инлайн критического CSS в теге <style>, затем неблокирующая загрузка полного файла.
<head>
<style>/* сюда содержимое dist/critical.css */</style>
<!-- полный CSS грузится, не блокируя рендеринг -->
<link rel="preload" href="/dist/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/dist/main.css"></noscript>
</head>Как это работает: rel="preload" с as="style" скачивает файл без блокировки, а onload переключает тег в обычный stylesheet, когда файл готов. Атрибут this.onload=null нужен, чтобы обработчик не сработал повторно в некоторых браузерах. Тег <noscript> — запасной путь для отключённого JavaScript.
Проверка: откройте страницу с throttling «Slow 3G» в DevTools. Контент первого экрана должен появиться сразу и в стилях, без прыжка. Полный CSS подтянется через сотни миллисекунд — визуально этого быть не должно, потому что критика уже покрыла видимое.
Критический CSS оправдан там, где первый экран важнее полноты стилей: лендинги, статьи, карточки товара. На SPA с рендерингом на клиенте выигрыш меньше — там первым делом грузится JS-бандл, и он же приносит стили через CSS-in-JS.
Где подход ломается
Инлайн раздувает HTML и не кешируется
Встроенный в <head> критический CSS повторяется в каждом HTML-ответе и не кешируется браузером отдельно. Если критика разрослась до 40–50 КБ, вы жертвуете кешируемостью и трафиком на повторных заходах ради первого рендера. Держите критику в пределах 14 КБ — это ориентир под первый TCP-пакет, за который иногда цепляются, хотя жёсткого правила тут нет. Важнее: если критики много, вы неправильно определили первый экран.
onload не срабатывает — стили не подключаются
Если полный CSS так и остался в rel="preload" и не переключился на stylesheet, страница останется с одной критикой навсегда. Причины: JS упал до выполнения обработчика, CSP заблокировал инлайновый onload, или файл вернул ошибку. Диагностика — во вкладке Elements посмотреть, стал ли rel равен stylesheet после загрузки. При строгом CSP используйте библиотеку loadCSS или навесьте обработчик отдельным скриптом вместо инлайнового атрибута.
Критика устаревает при каждом изменении дизайна
Извлечённый CSS — это снимок на момент сборки. Поменяли вёрстку первого экрана, забыли перегенерировать критику — получили частичный FOUC на изменившихся элементах. Извлечение критического CSS должно быть шагом сборки, а не разовой ручной операцией.
Динамический контент выше сгиба
Экстрактор рендерит страницу один раз в заданном вьюпорте. Если первый экран собирается на клиенте (персонализация, A/B-тест, ленивая подгрузка баннера), инструмент увидит не то, что увидит пользователь. Здесь критику придётся дособирать вручную для динамических блоков или отказаться от автоматизации на этих страницах.
Отложенная загрузка того, что не CSS
Тот же принцип preload применяют к шрифтам и не критичным ресурсам, но правила отличаются:
| Ресурс | Приём | Риск |
|---|---|---|
| Некритичный CSS | preload + onload → stylesheet | FOUC при неверной критике |
| Шрифты | preload as="font" + font-display: swap | прыжок текста при подмене шрифта (FOIT/FOUT) |
| Изображения ниже сгиба | loading="lazy" | сдвиг макета без заданных width/height |
| Изображения первого экрана | fetchpriority="high", без lazy | lazy на LCP-картинке тормозит рендер |
Частая ошибка — навесить loading="lazy" на главное изображение первого экрана, которое и есть LCP-элемент. Тогда браузер отложит его загрузку, и Largest Contentful Paint вырастет. Ленивую загрузку применяют только к тому, что ниже сгиба.
Что делать дальше
Встройте извлечение критического CSS в пайплайн сборки, чтобы оно запускалось при каждом деплое, а не руками. После внедрения замерьте First Contentful Paint и LCP в PageSpeed Insights до и после — на throttled-профиле, потому что на быстром канале разница теряется. Дальше отдельная тема — инвалидация кеша при обновлении CSS-файлов через хеш в имени; здесь мы её не разворачиваем.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: critical — GitHub repository (Addy Osmani), MDN — rel=preload
Частые вопросы
А если у меня Next.js или другой фреймворк с CSS-in-JS?
Что делать, если после внедрения появился FOUC?
Насколько большим может быть инлайновый критический CSS?
Работает ли rel=preload во всех браузерах?
Нужен ли критический CSS, если у меня небольшой сайт на 20 КБ стилей?
Мешает ли строгий Content Security Policy этому подходу?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.