@import в CSS: почему это медленно и чем заменить
Директива @import в CSS — частая причина задержки рендера. Разбираем как она работает, почему вредна для производительности и какие есть альтернативы.
CSS-директива @import url('...'); позволяет одному CSS-файлу подключить другой. Удобно для модульной организации стилей: основной файл подтягивает темы, типографику, медиа-стили. Удобно для разработчика, но почти всегда вредно для производительности.
В этой статье — как @import работает, почему он замедляет рендер, и чем его заменить.
Как браузер обрабатывает @import
Стандартный flow:
- Браузер получает HTML, парсит, находит
<link rel="stylesheet" href="main.css"> - Скачивает
main.css - Парсит
main.css, находит внутри@import url('theme.css') - Только теперь запрашивает
theme.css - Скачивает
theme.css - Парсит, если там тоже
@import— повторяет - Финально строит CSSOM, начинает рендер
Главная проблема — шаги 2-5 последовательны. Браузер не знает что нужен theme.css, пока не скачает и не распарсит main.css. Это блокирующая цепочка.
С <link> в HTML история другая — браузер видит все теги сразу при парсинге HTML, начинает скачивать все CSS параллельно. Существенная разница в финальном time-to-render.
Сколько это стоит
Конкретный пример. Допустим, у вас main.css весит 50 КБ, theme.css — 30 КБ. На быстром соединении каждый скачивается за 200 мс.
Через <link>:
- Браузер начинает оба запроса одновременно
- Через 200 мс оба готовы
- Общее время до начала рендера: ~250 мс
Через @import в main.css:
- Браузер скачивает main.css — 200 мс
- Парсит, обнаруживает @import
- Скачивает theme.css — ещё 200 мс
- Общее время до рендера: ~450 мс
Разница — почти в два раза. На медленном соединении (3G mobile) — разница 1-2 секунды. Это удар по LCP и Core Web Vitals.
Когда это особенно болезненно
Глубокие цепочки
Если у вас main.css импортирует theme.css, theme.css импортирует typography.css, typography.css импортирует variables.css — браузер обрабатывает их последовательно. На 4 уровнях глубины задержка может составить 1-2 секунды на быстром интернете.
Условные импорты
@import url('mobile.css') (max-width: 768px);
Кажется логичным — на мобильных не качать desktop-стили. Но браузер всё равно скачивает все CSS (он не знает заранее условие), парсит, и только потом решает применять или нет. Сэкономили на исполнении, но не на загрузке.
Импорты из CDN
@import url('https://cdn.jsdelivr.net/npm/normalize.css');
К дополнительному запросу добавляется DNS lookup и TCP-handshake к новому домену. На холодном соединении +300-500 мс.
Альтернативы
1. Множественные <link> теги в HTML
Старая школа, но работает:
<link rel="stylesheet" href="/css/normalize.css">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/theme.css">
Браузер начинает параллельную загрузку всех трёх сразу. Парсит по мере готовности. Объединённый CSSOM собирается раньше.
2. Сборщик объединяет CSS
Webpack, Vite, esbuild, Parcel — все они умеют склеивать CSS-файлы в один при сборке. Один HTTP-запрос вместо нескольких.
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
cssCodeSplit: false, // один CSS вместо разбивки
},
});
Минус: при изменении одной строчки в CSS меняется весь файл — пользовательский кеш протухает. Но обычно стили меняются редко, плюс не каждое изменение деплоится.
3. CSS Modules или CSS-in-JS
Современные React-стеки часто используют CSS Modules (Next.js по умолчанию) или CSS-in-JS (styled-components, emotion). Стили подключаются автоматически на уровне компонентов, без @import. Каждый компонент получает свой scope, сборщик объединяет в финальный bundle.
4. HTTP/2 push (deprecated, но был релевантен)
В эпоху HTTP/2 был механизм push — сервер мог отправить CSS-файл клиенту до того, как он его запросил. Это решало проблему @import-цепочек. С 2022 года Chrome убрал push (он создавал больше проблем, чем решал). Не используйте.
5. Inline критического CSS
Самое радикальное и эффективное решение. Стили above-the-fold блока инлайнятся прямо в HTML через <style>, остальные подгружаются асинхронно. Подробнее — в посте про render-blocking ресурсы.
Когда @import всё-таки можно
Несколько случаев когда @import оправдан:
- Шрифты с Google Fonts — в Google Fonts API сам файл подключается через
<link>, а внутри он использует@importдля подгрузки конкретных weight'ов. Это часть документированного API, не вы решаете. - Условные импорты по media-query — для печатных стилей, например
@import 'print.css' print;. Печатные стили не нужны при загрузке экрана, и браузер это понимает (загружает с низким приоритетом). - Динамические темы, переключаемые JavaScript'ом. Тогда «лишний» @import не критичен — он сработает только при переключении темы, не на первой загрузке.
Во всех остальных случаях — избегайте.
Как проверить ваш сайт
Откройте Chrome DevTools → Network → фильтр CSS. Загрузите страницу.
В колонке Initiator смотрите кто запустил запрос:
- Если CSS-файл инициирован «(index)» или HTML — это
<link>(хорошо) - Если инициирован другим CSS-файлом — это
@import(плохо)
Если видите цепочку CSS-файлов, инициированных друг другом — у вас @import. Стоит переделать.
Резюме
@import в CSS — атавизм из эпохи модульного CSS до сборщиков. В 2026 году с modern build pipeline (Webpack/Vite/etc) альтернатива всегда есть. Замена @import на параллельные <link> теги или склеенный bundle обычно ускоряет first paint на 200-1000 мс. Простая правка с прогнозируемым результатом.
Хотите проверить ваш CSS на цепочки и render-blocking? Напишите нам. Аудит производительности за 2 рабочих дня.