Привет Хабр! Недавно мне попался на рефакторинг один сайт написанный одним студентом. Он был реализован не лучшим образом и уже давно следовало бы его исправить. И вот наконец у меня выдалось свободное время на рефакторинг. Сайт был написан на Next.js, для написания стилей использовались SCSS Modules. А так как я на своих проектах уже давно использую Styled Components тут же в глаза бросился дискомфорт от использования обычного SCSS. И в этой статье я вам расскажу что же это за дискомфорт и как же Styled Components позволяет от него избавиться.

Сразу сделаю уточнение. Под Styled Components я имею ввиду не только библиотеку styled-components, а любую другую выполненную в концепции CSS-in-JS, например emotion, glamorous, styletron и подобные. Я же использую именно библиотеку styled-components потому что она имеет самое большое сообщество, отличную документацию, а в next.js уже встроена его поддержка.

А теперь о конкретных преимуществах.

Легко писать сложные компоненты

Представьте задачу. Вам нужно сделать банальную кнопку. Только ваша кнопка может использоваться в разных цветах, стилях, формах, может быть и заблокирована для нажатия.

Цветов может быть 4 варианта, стили 3 варианта, формы кнопки 3 варианта, размеры кнопки 3. В итоге кнопка может иметь 108 состояний. Как написать стили SCSS для такой кнопки? Скорее всего вы воспользуетесь методологией БЭМ напишите стили компонента и его модификаторы. И будет это выглядеть следующим образом:

.button {
    padding: 0.5rem 1rem;
    ...other styles...

    &[color="primary"] {
        color: #1976d2;
        background-color: #ffffff;
    }

    &...other colors... {}

    $[variant="contained"] {
        color: #ffffff;
        background-color: #1976d2;
    }
  
    &...other variants... {}

    $[disabled] {
        color: #b6b6b6;
        background-color: #e0e0e0;
    }
}

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

.button {
    padding: 0.5rem 1rem;
    ...other styles...

    $[variant="contained"] {
        color: #ffffff;
        background-color: #1976d2;
      
        $[disabled] {
            color: #b6b6b6;
            background-color: #e0e0e0;
      }
    }

    ...other styles...
}

Как же эту проблему решает Styled Components? А все просто. Вы пишите логику ровно так как вы привыкли это делать в Typescript.

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

interface IStyledButtonProps {
    color?: ButtonColors;
    variant?: ButtonVariants;
    disabled?: boolean;
}

export const StyledButton = styled.button<IStyledButtonProps>((props) => {

    // Set colors
    let primaryColor = "#1976d2";
    let secondaryColor = "#1976d2";

    if (props.color === ButtonColors.Secondary) {
        primaryColor = "other colors";
        secondaryColor = "other colors";
    } ...other colors...

    
    // Set styles
    let textColor = primaryColor;
    let bgColor = secondaryColor;

    if (props.variant === ButtonVariants.Contained) {
        textColor = secondaryColor;
        bgColor = primaryColor;
    } ...other variants...


    // Result CSS
    return css`
        color: ${textColor};
        background-color: ${bgColor};
        ...other styles...
    `;
});

Таким образом используя вместо бедного языка CSS мощный язык TypeScript вы на порядок упрощается сложность написания стилей. И вместо огромного каскада сложных перевложенных стилей получается простой и лаконичный код компонента.

Кроме того стили написанные на Styled Components, по моей практике, в среднем весят в 2-3 раза меньше чем стили написанные на SCSS. Что положительно влияет на размер бандла и скорость работы сайта.

Стили типизированы

Как видно из примера выше использовался язык TypeScript. А TypeScript это не только синтаксический сахар из будущего, но и мощнейший статический анализатор который не позволяет вам совершать ошибок еще на этапе написания кода.

И теперь у вас не просто мощный инструмент для написания стилей, но и автокомплит и статический анализатор который не позволит совершить ошибку в модификаторе. А так же сообщит об ошибке в случае изменения в API компонента.

Переход к стилям по горячей клавише

Так как теперь стили это обычные переменные TypeScript то и к ним теперь применяются все фичи TypeScript, в т.ч. переход по горячей клавише. Теперь не надо искать место где описаны стили, достаточно нажать всего одну кнопку и попасть сразу к месту со стилем и начать его править. Что безумно удобно и экономит много времени.

Так же действует и обратный механизм, имея переменную TypeScript вы можете легко найти все места использования данных стилей. Что сильно упрощает работу при рефакторинге.

Мертвые стили не попадают в сборку

У CSS есть большая проблема, все что попало в CSS остается в нем навсегда. Никто никогда не занимается чисткой CSS стилей. А по мере роста приложения и количества рефакторингов эта проблема начинает влиять на скорость сайта.

В тоже время все современные сборщики обладают механизмом удаления мертвого кода. А т.к. стиль это переменная то не используемые стили теперь не попадают в сборку. Кроме того такие стили видно глазом по счетчику ссылок, что позволяет удалять не актуальные стили и поддерживать код в чистоте.

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

Нету не используемых модификаторов стилей

В первом примере мы рассматривали кнопку. Когда мы пишем модификаторы для кнопки на SCSS то все написанные модификаторы попадают в сборку. Это плохо сказывается на скорости сайта, в т.ч. на SSR и скорость рендеринга стилей. Мы написали 108 состояний кнопки, но на странице используется только 1 состояние.

Styled Component работает по другому. Он в рантайме высчитывает стили и использует только те стили которые используются в данный момент. Таким образом если используется только одно состояние у кнопки то и в стили SSR попадет только одно состояние, а значит и у клиента страница прогрузится и отрисуется быстрее.

Можно использовать динамические стили

Если вы пишете компонент на CSS и в нем имеется вариативность, то вы не можете задавать в вариациях любые значения. Ведь эти стили необходимо собрать и включить в сборку. Поэтому в библиотеках как правило имеются заранее заложенные значения. Но вот проблема, эти значения не всегда подходят всем. А задавать любые значения не позволяет CSS, ведь их понадобится пересобрать из исходников.

Для примера. Я использую сеточный фреймворк. Все сеточные фреймворки имеют преднастройку на 12 колонок. А что делать если мне понадобилось 8 колонок? Пересобирать второй сеточный фреймворк для проекта?

К счастью в Styled Components нету такой проблемы и в стилях можно использовать не только те значения которые были заданы заранее, но и те которые передал пользователь через атрибуты. Теперь у меня есть сетка которая не имеет ограничений на колонки и я могу использовать именно те значения, которые использовал дизайнер при отрисовке макета.

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

Предотвращение конфликтов стилей

Styled Components не использует имен селекторов, вместо этого он использует хэшь от стиля. Примерно так же как это делает SCS Modules. Таким образом вам не надо бояться того что селекторы стилей начнут конфликтовать друг с другом.

Так же если два компонента будут иметь одни и те же стили то хешь у них будет одинаковый. В таких случаях Styled Components вставляет в DOM стили только один раз. Предотвращая дупликацию стилей и тем самым упрощает работу движку CSS браузера.

Позволяет использовать вложенные стили

Бывают ситуации когда внутри вашего компонента рисуется компонент из внешней библиотеки. Например внутри вашего блока рисуется слайдер от библиотеки Swiper. И вам необходимо поправить несколько стилей от этой библиотеки. Например перекрасить стрелочки из синих в красные.

В SCSS Modules вы столкнётесь с проблемой при такой задаче, поскольку он не позволяет писать вложенные стили.

В Styled Components вы без проблем можете написать вложенный стиль и стилизовать внешнюю библиотеку в нужном месте.

Производительность

К сожалению полноценного теста по производительности Styled Components найти не удалось, а делать самостоятельно слишком затратно. Но по личным ощущениям следующий тест отображает те же цифры которые получились у меня.

Источник: https://github.com/geeky-biz/css-in-js-benchmark
Источник: https://github.com/geeky-biz/css-in-js-benchmark

В чем проблема данного теста? Он тестирует только идеальный случай. Когда у вас есть только свежие стили и никакого мусора. В реальных же приложениях количества мусора в CSS быстро растет и начинает потреблять слишком много ресурсов. Styled Components же, как писал выше, вырезает мертвые стили из сборки. Поэтому с ростом приложения Styled Components будет все больше и больше выигрывать в плане перформанса.

Styled Components действительно дольше инициализируется за счет того что он работает в рантайме. Только у меня разница составляла не 40%, а всего 10%. Styled Components действительно быстрее рендерит изменения на страницах, за счет того что в движке браузера CSS гораздо меньше стилей и эти стили гораздо проще организованы для движка.

В сравнению с Linaria в Styled Components можно работать с динамическими компонентами и не зависеть от системы сборки.

CSS Modules может конкурировать со Styled Components только в плане инкапсуляции селекторов. При этом он не решает проблем написания сложных компонентов, удаления мертвых стилей и остальные проблемы.

Исходя из всего выше перечисленного Styled Components предоставляет отличный баланс между скоростью отрисовки и функциональностью.

Идеально для Микрофронтендов

Styled Components имеет очень маленький размер библиотеки, всего 35кб минифицированного кода. За счет чего он является отличным кандидатом для встраивания в микрофронтенд.

За счет инкапсуляции селекторов он предотвратит конфликты с основным приложением. За счет динамических стилей позволяет кастомизировать микрофронтенд под любые условия не прибегая к CSS переменным.

Вывод

Styled Components это не альтернатива CSS, Less, SCSS, CSS Modules. Это технология позволяющая писать CSS на совершенно новом уровне.

Переход с SCSS на Styled Components по своей эффективности можно сравнивать с переходом с html на jsx.

Считаете слишком приукрасил Styled Components? Готов поспорить в комментариях!

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


  1. grobitto
    31.12.2022 17:54
    +5

    Изначальная проблема с кнопками элементарно решается с помощью CSS variables.


    1. LabEG Автор
      31.12.2022 19:08
      -6

      Отчасти да. Но...

      1. Был кейс когда в проект встраивались два микрофронтеда с использованием одной и той же UI библиотеки на CSS переменных. В одной библиотеке эти переменные были переопределены и что привело к конфликту CSS переменных со вторым микрофрондендом. Пришлось тратить время что бы выкрутиться из конфликта. В Styled Components такая ситуация невозможна в принципе.

      2. Пример с кнопкой наверное слишком простой, но и с CSS переменными вы там на мучаетесь. У меня есть компонент Grid. Что бы он соответствовал сетке из Figma пришлось писать логику. В Figma сетка на десткопах имеет 12 колонок, планшетах 8 колонок, на мобилках 2 колонки. А еще 6 брекпойнтов по размеру экрана. А еще есть сетки вложенные в сетки. Например сетка с колонками 3 и 9, в 9 вложена сетка еще на 9 колонок. А еще в дефолтном положении она имеет 12 колонок. И в таком кейсе вам никакие CSS переменные не помогут. Styled Components справился с задачей идеально, на все расчеты уходит 1 наносекунда.


  1. gpaw
    01.01.2023 05:17
    +1

    извините, но это все очень спорно на больших проектах.

    ни один инструмент не ультимативен.

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

    верстаю больше 20 лет, я понимаю, что это не аргументный аргумент. тем не менее - этот подход, как любой другой - красив и удобен в своем случае.

    с наступившим новым годом, такой боевой настрой насчет фронта - вера в то, что есть энтузиасты) добра!


    1. LabEG Автор
      01.01.2023 15:44

      BEM приведен для примера как самый популярный подход написания компонентных стилей в CSS.

      Но как же вы решаете проблемы удаления мертвых стилей, простой переход к CSS селектору, поиск используемых стилей при рефакторинге, изоляции стилей и остального без использования SC?


  1. yroman
    01.01.2023 14:28

    >>Только у меня разница составляла не 40%, а всего 10%.

    Какие-то странные цифры - ни о чем. В каком окружении, на каком устройстве, в каком браузере?

    >> За счет динамических стилей позволяет кастомизировать микрофронтенд под любые условия не прибегая к CSS переменным.

    Задача. Есть компонент, который встраивается на страницу заказчика, который очень сильно захотел сделать его красненьким, а не зелененьким. В обычных условиях проблема элементарно решается через вынесение основных цветов в CSS переменные и добавлением на странице переопределенных значений этих переменных. Как решить задачу в случае styled components без пересборки бандла компонента?


    1. serginho
      01.01.2023 15:29

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


    1. LabEG Автор
      01.01.2023 15:33

      >> В каком окружении, на каком устройстве, в каком браузере?

      Это был тест. 1000 рандомно сгенерированных стилей и 1000 элементов на странице. Браузер Chrome. Процессор Ryzen 5800X.

      >> Как решить задачу в случае styled components без пересборки бандла компонента?

      На самом деле крайне странно слышать про пересборку SC с учетом того что это фактически JS и он полностью динамический в отличии от CSS.

      Для решения задачи есть два варианта:

      1. SC не лишает вас ничего из того что у вас есть в CSS. Поэтому можно использовать все те же CSS переменные =)

      2. Но почему конкретно я не использую CSS переменные во встраиваемых модулях. Есть такой антипаттерн - Глобальные переменные. Фронты уже поняли что в JS его использовать плохо, но почему то не поняли что в CSS переменных его использовать все так же плохо. А CSS переменные это глобальные переменные.

      В комментариях выше уже писал пример как я уже ловил багу из-за конфликта CSS переменных. Как эту проблему помогает решить SC? Значения в CSS назначаются через изолированные JS переменные.

      Компонент пишется следующим образом:

      export const config = {
        colors: {
          primary: "#000",
          secondary: "#001"
        }
      }
      
      export const Button = styled.div`
        color: ${() => config.colors.primary};
      `;

      А кастомизируется на стороне клиента следующим образом:

      import {config, Button} from "awesome-button";
      
      config.colors.primary = "#003";
      
      export const Page = () => (
        <div>
          <Button>
            Стильная кнопка
          </Button>
        </div>
      )

      Это упрощенный пример, но передает суть. При таком раскладе нету ни малейшего шанса на конфликт переменных.


      1. yroman
        01.01.2023 19:03

        Ну вот вам пример - у меня в проекте заказчик хотел всё то же, но с перламутровыми пуговицами. Как мне это кастомизировать, не залезая в JS код? То, что вы написали, это не кастомизация, а сборка под клиента. Просто вы так написали, как будто эти компоненты серебряная пуля от всех без, хотя это далеко не так.


        1. LabEG Автор
          01.01.2023 20:39

          Этот инструмент сильно упрощает написание стилей. Судя по вашим словам ваша задача лежит за пределами написания стилей.

          А ваша задача решается двумя способами. Либо компонент в пропсах принимает какие то кастомные элементы, которые подменяют дефолтные. За пример можно посмотреть MUI. Либо делать две независимые библиотеки.


  1. sarakusha
    02.01.2023 05:55

    Styled components и js-in-css абсолютно не работают в современном React 18 (streaming, Server components и все такое) и Next.js 13, если вы хотите использовать все эти последние фишки. Мой любимый MUI (material-ui), к сожалению тоже.


    1. LabEG Автор
      02.01.2023 22:16

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


  1. tropicalfruit
    02.01.2023 13:14

    Автор опоздал с написанием статьи так лет на 5. Когда все уходят от styled-components (не от css in js) из-за того, что стили генерятся в рантайме. Посмотрите на другие библиотеки и на то, как это сейчас решают в других компаниях, тот же Griffel от Майкрософта. Все понимают, что нужно использовать либы, которые позволяют генерировать стили "behind the runtime". Плюс SC не генерирует атомические стили.


    1. LabEG Автор
      02.01.2023 22:22

      Что бы уйти от SC надо к нему с начало прийти =)

      Надо понимать что SC распространен только в мире React. Angular и Vue разработчикам он не доступен. Да и в мире React он распространен только на 25% примерно. У многих он вызывает религиозную неприязнь, что ожидаемо сказалось на карме статьи.

      А от рантайме уходить очень не удобно, потому что динамические стили часто сильно упрощают работу.

      Кстати вспомнил. В статье целый раздел забыл =)

      Бывают стили написанные следующим образом:

      .img-1 {
          left: 1rem;
          top: 1rem;
      }
      
      ...
      
      .img-27 {
          left: 13rem;
          top: 9rem;
      }

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

      export const FloatImage = styled.img`
          left: ${(props) => props.left ?? 0}rem;
          top: ${(props) => props.top ?? 0}rem;
      `;


  1. antonkrechetov
    02.01.2023 14:17

    Вы описали все модификатора цвета, вы описали все модификатора вариантов, вы закрасили кнопку серым в случае если она заблокирована, но тут вы понимаете что в разных вариантах кнопка блокировка выглядит по разному, где то фон залит серым, где то белым. И тут вы начинаете множить стили, вкладывать стили блокированной кнопки в варианты кнопок. Код начинает экспоненциально разбухать. И вот уже банальная кнопка превращается в файл стилей на тысячи строчек, а там еще и проблемы с приоритетами стилей начинают всплывать.
    Если необходимо сделать заблокированную кнопку более блеклой, это решается обычно прозрачностью (opacity) или, например, с помощью ::after с бэкграундом и свойством mix-blend-mode, которое хорошо поддерживается современными браузерами. Плюс кастомные свойства, как правильно написали выше.

    Но, если вы всё-таки не хотите знать ничего новее color и background-color, всю генерацию стилей можно сделать и в scss. Если что, в scss можно писать функции, списки и циклы. Это Тьюринг-полный язык.


    1. LabEG Автор
      02.01.2023 22:27

      Пример с кнопками взят из MUI. Кстати он написан на аналоге SC, библиотеке emotion. В той кнопке цвета меняются местами в зависимости от варианта кнопки. А это строго переменные CSS или JS.


  1. ShadowOfCasper
    03.01.2023 06:27

    Styled components как и весь css in js - большой неповоротливый костыль. Всё то же самое проще некуда решается на tailwind без необходимости писать стили вообще.

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

    О том почему же не стоит использовать styled я могу написать статью и пожирнее