javascript

CSS-in-JS vs CSS Modules: что выбрать в 2026?

  • пятница, 24 апреля 2026 г. в 00:00:38
https://habr.com/ru/articles/1025688/

В 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 КБ

Формат стилей

.css-файлы

Inline <style>

.css-файлы

Поддержка 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 не имеет такой проблемы, поскольку стили статические.

Практические примеры


Рассмотрим немного подробнее использование описанных вариантов стилизации, на примере создания и использования кастомных тем для приложения. 

  1. 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, если 

  1. Вы работаете с React Server Components

  2. Для вас критичны FCP, LCP, CLS и SEO

  3. Вы не хотите добавлять ни байта JavaScript ради стилей

Выбирайте zero-runtime CSS-in-JS, если 

  1. Вы тоже работаете с RSC и хотите типобезопасность

  2. Вам нужна строгая типизация стилей

  3. Вы привыкли к синтаксису CSS-in-JS, но не хотите платить рантаймом

Выбирайте runtime CSS-in-JS, если 

  1. Вы точно не используете RSC

  2. У вас небольшой или средний проект, где простота разработки перевешивает метрики производительности