Мне нравится удобство CSS-in-JS, особенно возможность совместного расположения стилей, но в некоторых деталях я не уверен:
- В том, что обязательно нужно использовать хешированные классы вместо классов пространств имён. А представьте, как может раздражать их присутствие в сторонних компонентах!
- В том, что применение логики в CSS всегда лучше или более удобно для восприятия человеком. А также это даёт мощный рост производительности (подробнее об этом ниже).
- И наконец, в том, что добавлять 25–40 Кбайт минифицированного кода JavaScript к npm-компоненту размером в 10 Кбайт — это хорошая идея.
Поначалу всё хорошо и беззаботно и никаких проблем с производительностью этих CSS-решений с «невероятно быстрым» временем выполнения не замечается. Но это до тех пор, пока вы не поместите всех их на одной странице в Storybook.
Обратимся к вездесущему компоненту Button
, имеющему различные стили и параметры:

У нас там было множество простых и быстрых функций для создания стилей. Мы даже оптимизировали статический css, выделив его в самостоятельный фрагмент (sharedStaticButtonStyles
):

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

Около 36 секунд ушло у Emotion
на выполнение парсинга (см. подчёркнутое красным). Инструмент производительности Performance делает это примерно в 2–3 раза быстрее, но всё равно это слишком долго.
Дальше всё только медленнее
Сначала я думал, что проблема в компоненте Tooltip
(всплывающая подсказка): может быть, это он так медленно отображается? А затем понял, что это происходит, только когда он прикреплён к Button
. Стало быть, Emotion
при наведении курсора снова выполняет парсинг CSS (потому что Tooltip
вызывает повторное отображение) и приостанавливает основной поток примерно на 900 миллисекунд, прежде чем появится всплывающая подсказка!

Вы можете подумать: «Ну мои компоненты не такие сложные и медленные». Даже если они в два раза быстрее в крупном приложении с большим количеством компонентов на странице, тратить 1–2 лишних секунды на загрузку, пока выполняется парсинг CSS, недопустимо. Особенно в высокодинамичных приложениях, получающих большое количество обновлений в секунду. В них Emotion
выполняет повторный парсинг при каждом отображении (как это происходит при наведении курсора), что обусловлено динамической природой стилевого оформления с использованием пропсов.
Как же сделать всё быстрым?
Во-первых, если вы начинаете новый проект, стоит подумать о CSS-in-JS решениях времени компиляции: таком или таком.
Если речи о новом проекте не идёт, можно увеличить производительность уже имеющегося стилевого оформления до 175 раз, сделав CSS более статичным.
Уже написано несколько статей о том, как использование пропсов при тематическом оформлении засоряет приложение специальными обёртками.
Поэтому если бы я начинал новый проект, то для тематического оформления вместо пропсов использовал бы CSS-переменные.
Но узкое место производительности не в этом
А в тех вызовах вложенных функций в css, которые принимают вдобавок к теме ещё и пропсы:

Я не рассматривал подробно CSS-код во время выполнения, но мне кажется, что при вызове функция фактически заявляет: «Я не знаю, что она вернёт, так что буду пересчитывать её при каждом отображении».
На помощь приходят атрибуты данных и CSS-переменные
Давайте поменяем то, как мы делаем этот компонент Button
:

Нас здесь интересует использование CSS-переменных, позволяющее резко сократить объём статического CSS, который необходимо написать.
В ходе выполнения рефакторинга старые функции для получения цветов и размеров были сохранены и перемещены из Emotion
CSS в CSS-переменные почти 1 к 1:

Эта функция выполняется в первый раз примерно за 0,002 миллисекунды. Посмотрим, как теперь выглядит CSS компонента Button
:

Помните, на парсинг уходило 36 секунд? Посмотрим, что сейчас:

А теперь парсинг CSS выполняется чуть больше чем за 200 миллисекунд. В то время как Performance делал это всего в 2–3 раза быстрее (т. е. за 12–18 секунд).
Подведём итоги:
- В новых проектах используйте CSS времени компиляции.
- Для тематического оформления задействуйте в новых проектах CSS-переменные.
- Делайте CSS как можно более статичным для вариаций (это самая важная оптимизация).
Читайте также:
- Точки останова CSS в Material UI
- Динамическое масштабирование элементов в CSS
- Чего я не знал о CSS, а стоило бы
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Dominic Tobias: How to increase CSS-in-JS performance by 175x