
В 2026 году фронтенд-разработка продолжает развиваться: появляются новые фреймворки, улучшаются инструменты сборки, растут требования к производительности и пользовательскому опыту.
Разработчики сталкиваются с выбором: использовать CSS Modules или CSS-in-JS решения. Эти подходы дают изоляцию стилей и интеграцию с компонентами, но различаются по реализации и ограничениям.
Выбор системы стилизации влияет на разработку и ключевые метрики: размер бандла, скорость первого рендера, поведение при SSR, удобство отладки и поддержку кода. Неподходящий подход может привести к увеличению объёма JavaScript, проблемам с SSR и усложнению масштабирования.
Данная статья не ставит цель назвать одного победителя. Вместо этого мы сравним основные подходы - CSS Modules и CSS-in-JS:
как они влияют на производительность и размер бандла,
насколько комфортно с ними работать в команде,
как ведут себя при серверном рендеринге,
какие компромиссы неизбежны в каждом случае.
Что такое CSS Modules и CSS-in-JS?
Прежде чем сравнивать производительность или удобство, важно понять, о каком типе стилизации вообще идёт речь.
Оба подхода, упомянутые в названии, относятся к так называемой scoped-стилизации на уровне компонента - то есть каждый компонент владеет своими стилями, и они не влияют на остальное приложение.
Это отличается от utility-first или глобальных CSS-фреймворков (например, Bootstrap или Tailwind CSS), где стили задаются через универсальные утилитарные классы вроде text-center, bg-blue-500 или p-4. Такие классы глобальны по умолчанию и переиспользуются по всему проекту.
CSS Modules
CSS Modules - это не библиотека, а методология, реализуемая на этапе сборки (например через Webpack или Vite). Основная идея проста: каждый CSS-файл, подключенный к компоненту, обрабатывается так, чтобы все его классы стали локальными по умолчанию. Это решает главную проблему глобального CSS — конфликты имён.
Например, файл Button.module.css с классом .primary на выходе превращается в нечто вроде .Button_primary__xY2z9. React-компонент импортирует эти «хэшированные» имена как объект и использует их в className.
Ключевая особенность: стили остаются обычным CSS, а изоляция достигается без JavaScript-рантайма.
CSS-in-JS
Термин CSS-in-JS часто ассоциируется с библиотекой Styled Components, но на самом деле это широкая парадигма, которая в 2026 году делится на два разных направления:
1. Runtime CSS-in-JS
Styled Components или emotion - здесь стили генерируются в браузере во время выполнения. Каждый компонент динамически создаёт CSS-правила и вставляет их в <style>-теги.
Пример Styled Componetnts
import styled from ‘styled-components’ const Button = styled.button` background: ${props => props.primary ? '#007bff' : '#6c757d'}; color: white; border: none; padding: 8px 16px; `;
Такой подход удобен для динамических стилей, но требует рантайм-библиотеки, увеличивает JS-бандл и усложняет SSR.
2. Zero-runtime CSS-in-JS
Linaria или vanilla-extract - стили пишутся на JavaScript, но полностью компилируются в статические CSS-файлы на этапе сборки. В рантайме остаётся только подключение классов — как в CSS Modules.
Пример vanilla-extract
import { style } from '@vanilla-extract/css'; export const primary = style({ background: '#007bff', color: 'white', padding: '8px 16px', selectors: { '&:hover': { opacity: 0.9 } } });
Здесь вы получаете типобезопасность, темизацию на этапе сборки, динамические классы — и при этом нулевой JS-оверхед.
Тип |
Примеры |
Где обрабатываются стили? |
Рантайм |
Runtime CSS-in-JS |
Styled Components, Emotion |
В браузере |
Да |
Zero-runtime CSS-in-JS |
Linaria, vanilla-extract |
На этапе сборки |
Нет |
Все два подхода в этой статье объединяет одно - стиль принадлежит компоненту. Он не «утекает» и не зависит от глобального состояния CSS. Это критически важно в крупных приложениях, дизайн-системах и библиотеках компонентов - там, где предсказуемость и инкапсуляция важнее скорости прототипирования.
Производительность и размер бандла
Выбор между CSS Modules и CSS-in-JS - напрямую влияет на производительность приложения, опыт пользователя и технический долг. В 2026 году, когда Google учитывает метрики вроде LCP и CLS при ранжировании, даже пара лишних килобайт JavaScript или неоптимальная инъекция стилей могут стоить вам трафика.
Рассмотрим три варианта, с точки зрения бандла, рендера и совместимости с SSR:
CSS Modules
Runtime CSS-in-JS (на примере Styled Components / Emotion)
Zero-runtime CSS-in-JS (на примере vanilla-extract / Linaria).
CSS Modules: минимализм и предсказуемость
CSS Modules не добавляют никакого JavaScript-рантайма. Стили компилируются в отдельные .css-файлы, которые:
подключаются через <link rel="stylesheet">,
кэшируются браузером,
не блокируют выполнение JS (если загружены асинхронно или критически извлечены).
Особенности
0 КБ JavaScript на стили.
Идеальная совместимость с SSR: стили приходят сразу в HTML.
Легко оптимизировать critical CSS.
Поддержка code splitting «из коробки», при использовании Webpack/Vite
Runtime CSS-in-JS: удобство ценой рантайма
Библиотеки вроде Styled Components или Emotion генерируют CSS в браузере. Это дает невероятную гибкость, но имеет цену:
Styled Components весит ~14 КБ (min + gzip), Emotion - чуть легче (~10 КБ)
При SSR без настройки:
стили не попадают в HTML,
возникает FOUC (вспышка нестилизованного контента),
клиенту приходится ждать загрузки JS, чтобы увидеть оформление.
Zero-runtime CSS-in-JS: компиляция как преимущество
Библиотеки вроде vanilla-extract и Linaria предлагают лучшее из обоих миров:
синтаксис и типобезопасность JavaScript,
нулевой рантайм - стили компилируются в статические .css-файлы.
Например, vanilla-extract: генерирует CSS на этапе сборки, экспортирует только имена классов, не требует установки в dependencies (только devDependencies).
Особенности:
0 КБ JavaScript на стили.
Полная поддержка SSR и гидратации.
TypeScript «понимает» стили - автодополнение, рефакторинг, безопасность.
Темизация через compile-time переменные.
Показатель |
CSS Modules |
Runtime CSS-in-JS |
Zero-runtime CSS-in-JS |
JS-оверхед |
0 КБ |
10-14 КБ |
0 КБ |
Формат стилей |
|
Inline |
|
Поддержка SSR |
Отличная |
Требует настройки |
Отличная |
Critical CSS |
По умолчанию |
Требует настройки |
По умолчанию |
Влияние на FCP / LCP |
Минимальное |
Умеренное / высокое |
Минимальное |
Кэширование стилей |
Да |
Нет |
Да |
Рассмотрим некоторые показатели подробнее, например Critical CSS:
CSS Modules и zero-runtime CSS-in-JS: современные сборщики умеют автоматически извлекать critical CSS или подключать стили по чанкам. Runtime CSS-in-JS: все стили генерируются динамически, то есть невозможно заранее знать, какие правила понадобятся и critical CSS приходится извлекать вручную.
Теперь посмотрим на проблему с SSR у runtime css-in-js
Допустим мы создаем стилизованную кнопку.
const Button = styled.button` background: blue; color: white; `;
В браузере при первом рендере Styled Components генерирует CSS-правило, создается уникальный класс (что-то вроде, sc-bdVaJa), который динамически вставляется в <head> через тег <style>.
На сервере (при SSR) та же логика не срабатывает автоматически, потому что: нет DOM, нет <head>, нет механизма «собрать все стили, использованные при рендере».
Поэтому браузер будет рисовать обычную кнопку без стилей и только после загрузки и выполнения всего JavaScript-бандла (включая Styled Components) появляется <style>-тег и кнопка получает заданные стили.
Чтобы исправить эту проблему, Styled Components и Emotion предоставляют специальные API для SSR, но они требуют ручной настройки:
Пример для Styled Components
import { renderToString } from 'react-dom/server'; import { ServerStyleSheet } from 'styled-components'; const sheet = new ServerStyleSheet(); const html = renderToString(sheet.collectStyles(<App />)); const styleTags = sheet.getStyleTags(); const fullHtml = ` <html> <head>${styleTags}</head> <body><div id="root">${html}</div></body> </html>
Здесь мы заранее генерируем css в виде строки (sheet.getStyleTags()) и вставляем его в <head>
Но такой подход тоже имеет свои нюансы, например в Next.js это будет ломать SSR из коробки, потребуется настройка кастомного Document (_document.js)
Если не производить этой настройки, то получаем проблемы с метриками:
FCP (First Contentful Paint): откладывается, пока не загрузится JS и не применятся стили.
CLS (Cumulative Layout Shift): скорее всего страница будет “прыгать” после применения стилей
SEO: поисковики могут проиндексировать нестилизованный контент, особенно если рендеринг медленный.
React Server Components
После появления React Server Components и App Router в Next.js подход к построению фронтенд-приложений изменился: часть компонентов теперь выполняется на сервере и не попадает в клиентский бандл.
И здесь CSS Modules демонстрируют абсолютную совместимость, в то время как runtime CSS-in-JS сталкиваются с ограничениями.
CSS Modules
CSS-файлы обрабатываются на этапе сборки (Vite/Webpack). Классы, по типу .card превращается в уникальный хэш (например, ProductCard_card_1xY2z). В Server Component импортируется просто строка с этим классом. Никакого React-рантайма, никаких хуков, никакого контекста.
Выходит, что CSS Modules - золотой стандарт для серверных компонентов.
Runtime CSS-in-JS
Runtime CSS-in-JS полагается на React Context для передачи темы и на runtime-инъекцию стилей. Это не будет работать в серверных компонентах, потому что те имеют жесткое ограничение:
Нет состояния (useState, useReducer)
Нет контекста (React Context не доступен)
Нет хуков жизненного цикла (useEffect, useLayoutEffect)
Нет событий (onClick, onSubmit)
Обходной путь в таком случае может быть следующим: выносить отдельно стилизованные компоненты и использовать ‘use client’, но стили все равно будут рантаймовыми.
zero-runtime CSS-in-JS не имеет такой проблемы, поскольку стили статические.
Практические примеры
Рассмотрим немного подробнее использование описанных вариантов стилизации, на примере создания и использования кастомных тем для приложения.
Runtime CSS-in-JS, на примере styled-components
Темы задаются объектом, с необходимыми цветами, например:
export const lightTheme = { colors: { primary: '#007bff', } } export const darkTheme = { colors: { primary: '#4d9eff', } }
Тему можно передавать через ThemeProvider, обернув в него приложение, управлять текущей темой можно, например стейт менеджером или обычным useState
import { ThemeProvider } from 'styled-components'
Использовать внутри стилизованных компонентов.
const StyledButton = styled.button` background-color: ${({ theme }) => theme.colors.primary}; `
У этой реализации есть одна небольшая проблема — редактор кода не даёт подсказок при написании свойств theme. Для решения этой проблемы, понадобится Typescript - типизировать объекты с темами, и расширить стандартный интерфейс темы собственным.
Zero-runtime CSS-in-JS, на примере vanilla-extract
Темы также создаются в виде объектов.
export const themeContract = createThemeContract({...}) export const lightTheme = createTheme(themeContract, { colors: { primary: '#007bff', } }) export const darkTheme = createTheme(themeContract, { colors: { primary: '#4d9eff', } })
Используются для стилизованных компонентов, через созданный themeContract
export const button = style({ background: themeContract.colors.primary, })
Динамическая смена темы происходит через CSS-классы на корневом элементе.
const themeClass = theme === 'light' ? lightTheme : darkTheme <html lang="ru" className={themeClass}>
CSS Modules
Посмотрим на реализацию тем для приложения, через data атрибуты.
Для каждой темы создаются соответствующие файлы, в них задаются css переменные, следующим образом:
:root[data-theme="light"] { --color: #007bff; } :root[data-theme="dark"] { --color: #4d9eff; }
Созданные переменные можно использовать внутри модулей.
button { background-color: var(--color); }
Для переключения темы создается кастомный ThemeProvider, для хранения темы можно использовать useState, а для переключения необходимо указывать соответствующий data атрибут.
document.documentElement.dataset.theme = 'dark'
Итоги
CSS Modules — простой и стабильный вариант: не добавляют JavaScript и корректно работают с SSR и серверными компонентами.
Zero-runtime CSS-in-JS — позволяют писать стили в JavaScript, но без рантайма: всё компилируется в обычный CSS на этапе сборки.
Runtime CSS-in-JS — дают больше гибкости за счёт генерации стилей в рантайме, но увеличивают размер JavaScript и требуют дополнительной настройки для SSR и RSC.
Выбирайте CSS modules, если
Вы работаете с React Server Components
Для вас критичны FCP, LCP, CLS и SEO
Вы не хотите добавлять ни байта JavaScript ради стилей
Выбирайте zero-runtime CSS-in-JS, если
Вы тоже работаете с RSC и хотите типобезопасность
Вам нужна строгая типизация стилей
Вы привыкли к синтаксису CSS-in-JS, но не хотите платить рантаймом
Выбирайте runtime CSS-in-JS, если
Вы точно не используете RSC
У вас небольшой или средний проект, где простота разработки перевешивает метрики производительности