В 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 Components

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 хорошо подходят для Server Components, потому что не требуют клиентского рантайма и работают на уровне сборки.

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. У вас небольшой или средний проект, где простота разработки перевешивает метрики производительности

Комментарии (14)


  1. Alexandroppolus
    23.04.2026 13:52

    У css-modules есть досадный минус: из коробки они плохо типизированы. Если написать import styles from './Comp.module.css'; , то styles оказывается просто Record<string, string>. Есть какие-то дополнительные нашлепки, которые генерят *.d.ts, но это так себе.

    На текущем проекте используем vanilla-extract, вполне устраивает.


    1. winkyBrain
      23.04.2026 13:52

      https://marketplace.visualstudio.com/items?itemName=clinyong.vscode-css-modules не решает проблему проверки наименований классов на уровне тайпскрипта(да и с чего бы), но в IDE позволяет парсить модули и обращаться к классам уверенно через styles.**** с автокомплитом


  1. vmkazakoff
    23.04.2026 13:52

    Блин, интересно, веб когда нибудь вернётся в просто нормальным стилям? Бесит же, когда стили на сайте весят мегабайт просто потому что для каждой из сотен button сгенерился свой .buttin_uf5gj8h класс, хотя они все почти одинаковые.

    Почему-то на бэке сильнее задумываются об оптимизации, а на фронте все просто обмазывают очередной абстракцией и новым слоем проблем.


    1. NextKolya
      23.04.2026 13:52

      turbopack в next вообще не позволяет изменять стили от css модулей, при этом генерируя их максимально больными для глаз. Вот пример из production бандла - 'FullNavbar-module-scss-module__DJfi9G__full-navbar'.
      Решается это всё доступом к настройке генерируемых классов, но большинство решений почему-то это не предоставляет


      1. vmkazakoff
        23.04.2026 13:52

        В идеале бы какой-то сборщик, который сможет собрать классы, что есть цвет фона и шрифта, есть скругления, есть тени и их разное сочетание даёт кнопки, карточки, модалки...

        Хотя я знаю такой сборщик. Называется "человек разумный")))

        Не, вообще ИИ с этим тоже справится, но приходится его прям гонять "нет, ты сделал лишние классы, когда такие уже были, переиспользуй" и "а теперь отрефакторь, чтобы убрать лишнее и соблюдать dry принцип"


    1. Driver86
      23.04.2026 13:52

      Блин, интересно, веб когда нибудь вернётся в просто нормальным стилям?

      У меня такая же мысль возникла после прочтения статьи. За последнее время веб очень усложнился, оброс новыми проблемами которые как-то решать надо, а том числе огромный вес.


      1. vmkazakoff
        23.04.2026 13:52

        Честно говоря я даже не уверен, что их надо именно решать, а не создать просто все заново. Ванильный html, ванильный js и простой css сейчас решает кучу проблем, но большинство разработчиков (особенно джун/мидл) сегодня уже просто не умеют что делать без react. Сеньеры кто постарше ещё умеют. Пока...

        Это боль. Они на полном серьёзе сравнивают css-in-js с модулями, а вариант что вообще можно просто руками написать style.css который будет весить 5кб - этого тупо нет в картине мира...


        1. winkyBrain
          23.04.2026 13:52

          Авторитетное мнение бэкенда по поводу фронта) идите сделайте пожалуйста авито или вк на html и ванильном js, а мы посмотрим. Вы вот даже не понимаете, чем css-in-js плох с точки зрения взаимодействия с ним - вам и не надо, не сталкиваетесь же. Но мнение есть, и уверенное) как-то знаете, жидко что ли, непрофессионально


          1. vmkazakoff
            23.04.2026 13:52

            Я фуллстэк, делал достаточно крупные решения в направлении обучения в Сбере, Райфе и МТС. Ваше жиденькие можете оставить себе.

            На ванильных решениях я с командой делал в том числе решения которые дальше продавались на рынок корп.клиентам, на десятки экранов.

            Рецепт очень простой: на собеседовании проверять, что кандидат умеет вообще в фронтенд, а не только в реакт. Правда для этого надо самому шарить.


            1. winkyBrain
              23.04.2026 13:52

              Фуллстек, на моём опыте, в 100% случаев значит "я знаю бэк и чё-то там щупал пару раз во фронте". В билайн при мне фуллстэк-тимлид делал отступы между блоками белыми бордерами на белом фоне - это всё, что нужно знать о компетенции фуллстека относительно фронтенд-разработки и вёрстки в частности) нет, если вам нравится рассуждать, мало разбираясь в теме - пожалуйста, кто ж запретит. Только стоит понимать, что у этих рассуждений мало общего с реальностью

              Рецепт очень простой: на собеседовании проверять, что кандидат умеет вообще в фронтенд, а не только в реакт. Правда для этого надо самому шарить.

              Здесь у нас одна позиция: достойный фронтенд это в первую очередь знание основ и умение их использовать, а потом уже фреймворки и прочие навороты


    1. Sasha_Berg
      23.04.2026 13:52

      Тот же больной вопрос.


    1. winkyBrain
      23.04.2026 13:52

      для каждой из сотен button сгенерился свой .buttin_uf5gj8h класс, хотя они все почти одинаковые

      Решается созданием своей дизайн-системы/использованием существующей. Честно не представляю, где вы нашли файл со стилями на мегабайт. Уж столько неиспользуемого css мне приходилось вычищать - сам итоговый файл со стилями редко превышал хотя бы 100 кб. И это будет огрооомный файл)


  1. cmyser
    23.04.2026 13:52

    Не одним реактом едины https://habr.com/ru/articles/523646/

    Есть ещё подход css in TS где не только селекторы но и сама структура типизируется

    можно написать

    { Details: { Body: { overflow: ‘overlay’ } } } а TS проверит что у компонента есть элемент Details и что Details содержит Body и что Body компонент, а не строка

    Плюс человекочитаемые имена вместо хэшей, что гораздо удобнее

    Пример того что генериться : [my_profile_details_body]

    Ещё плюс: всё селекторы специфичны, но легко перебиваются откуда угодно, очень гибко


  1. smple
    23.04.2026 13:52

    если статью не читать то впринципе ии справился с заданием.

    если читать там одни и теже выводы по несколько раз встречаются это еще я половину прочитал.

    любой современный ии намного лучше иныормацию выдаст если просто title статьи ему отправить.