CSS Modules или CSS-in-JS: сравнение и выбор в 2026

1 апреля 2026 · ? просмотров · ? мин
монитор на темно-фиолетовом фоне с ярким курсом
Содержание
В 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 - cтили пишутся на 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-оверхед.
Все два подхода в этой статье объединяет одно - стиль принадлежит компоненту. Он не «утекает» и не зависит от глобального состояния 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 переменные.
Рассмотрим некоторые показатели подробнее, например 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. У вас небольшой или средний проект, где простота разработки перевешивает метрики производительности
Оценить материал
Остальные статьи по react