Технология CSS-in-JS заняла прочное место среди инструментов фронтенд-разработки. И возникает ощущение, что CSS-in-JS-тренд в ближайшем будущем лишь усилится. Особенно — в мире React. Например, в исследовании State of CSS, проведённом в 2020 году, приняли участие 11492 человека. Лишь 14,3% из них не слышали о Styled Components (о ведущей CSS-in-JS-библиотеке). А вот пользовались этой библиотекой более 40% участников исследования.



Мне уже давно хотелось найти серьёзный материал, посвящённый сравнению производительности CSS-in-JS-библиотек, вроде Styled Components, и доброго старого CSS. Но я, к сожалению, ничего такого, вроде сравнения их производительности на реальном проекте, а не на каком-то простом наборе тестов, найти не смог. Поэтому я решил сам сделать такое сравнение. Я перевёл реальное приложение со Styled Components на Linaria, на библиотеку, которая выполняет извлечение CSS в файлы во время сборки проекта. В результате в приложении, использующем Linaria, не выполняется генерирование стилей во время работы этого приложения на компьютере пользователя.

Прежде чем мы приступим к делу — хочу прояснить некоторые вещи. Я не отношу себя к людям, которые ненавидят CSS-in-JS. Я признаю то, что эта технология отличается отличным «опытом разработчика» (Developer Experience, DX), и то, что она обладает замечательной моделью композиции, унаследованной от React. CSS-in-JS способна дать разработчикам много хорошего (почитать об этом можно здесь). Да и я сам пользуюсь библиотекой Styled Components в нескольких собственных проектах и в проектах, над которыми мне доводилось работать. Но мне всегда было интересно знать о том, сколько пользователям веб-проектов приходится платить за те удобства, которые даёт разработчикам CSS-in-JS.

Да, если вас интересуют лишь мои выводы — то вот они: не используйте CSS-in-JS с вычислением стилей во время работы программы в том случае, если вы заботитесь о скорости загрузки вашего сайта. Тут всё просто: чем меньше JavaScript-кода — тем быстрее сайт. И с этим ничего особо поделать нельзя. Если же вам интересно узнать о том, как я пришёл к таким выводам — продолжайте читать.

Что и как я измерял


Приложение, которое я использовал в тестах — это вполне обычный React-проект. Его основа создана с помощью Create React App (CRA), в нём используется Redux и Styled Components (v5). Это — достаточно большое приложение с множеством экранов, с настраиваемой панелью управления, с поддержкой тем и со многими другими возможностями. Так как оно было создано с помощью CRA — оно не поддерживает серверный рендеринг, в результате всё рендерится на стороне клиента (речь идёт о B2B-приложении, в перечне требований к нему серверного рендеринга не было).

Я взял это приложение и заменил Styled Components на библиотеку Linaria, которая, как мне казалось, имеет похожий API. Я полагал, что перейти со Styled Components на Linaria будет просто. Но, на самом деле, перевод приложения на новую библиотеку стилизации потребовал определённых усилий. А именно, на то, чтобы перевести приложение на Linaria, у меня ушло два месяца. Но даже после того, как у меня получилось что-то такое, с чем уже можно было работать, переведены были лишь несколько страниц, а не всё приложение. Подозреваю, что именно поэтому никто и не проводит таких сравнений, которое решил провести я. Единственное изменение, которое я внёс в приложение, было представлено заменой одной библиотеки стилизации на другую. Всё остальное осталось нетронутым.

Для запуска различных тестов, направленных на исследование двух страниц, которые используются чаще всего, я пользовался инструментами разработчика Chrome. Я всегда запускал тесты по три раза. Представленные здесь цифры — это средние показатели по трём запускам тестов. Во всех тестах я устанавливал, на вкладке Performance, значение 4x slowdown для параметра CPU и значение Slow 3G для параметра Network. Для исследования производительности я использовал отдельный профиль Chrome без каких-либо расширений.

Вот какие испытания я провёл:

  1. Анализ сетевой активности приложения (размер JS- и CSS-ресурсов, анализ используемого кода, количество запросов).
  2. Исследование производительности в Lighthouse (аудит производительности с применением мобильных предустановок).
  3. Профилирование производительности (исследование загрузки страниц и особенностей drag-and-drop-взаимодействия с ними).

Анализ сетевой активности приложения


Начнём с анализа сетевой активности приложения. Одной из сильных сторон CSS-in-JS является тот факт, что при использовании этой технологии в приложение не попадает ненужных стилей. Верно? Ну, на самом деле, это не совсем так. Когда на странице имеется лишь один активный стиль, вместе с ним могут загрузиться и ненужные стили. Но эти стили находятся не в отдельном файле, а в JS-бандле.

Вот данные, полученные при исследовании домашней страницы двух вариантов приложения. Один из них, напомню, создан с использованием Styled Components, а второй — с помощью Linaria. Показатель до косой черты — это размер данных, сжатых gzip, а после косой черты идёт размер несжатых данных.

Сравнение сетевых показателей домашней страницы двух вариантов приложения.

Styled Components Linaria
Общее количество запросов 11 13
Общий размер 361Кб/1,8MB 356Кб/1,8Мб
Размер CSS 2,3Кб/7,2Кб 14,7Кб/71,5Кб
Количество CSS-запросов 1 3
Размер JS 322Кб/1,8Мб 305Кб/1,7Мб
Количество JS-запросов 6 6

Сравнение сетевых показателей поисковой страницы двух вариантов приложения.

Styled Components Linaria
Общее количество запросов 10 12
Общий размер 395Кб/1,9Мб 391Кб/1,9Мб
Размер CSS 2,3Кб/7,2Кб 16,0Кб/70,0Кб
Количество CSS-запросов 1 3
Размер JS 363Кб/1,9Мб 345Кб /1,8Мб
Количество JS-запросов 6 6

Даже несмотря на то, что в Linaria-варианте приложения значительно возрос объём загружаемого CSS-кода, общий объём загружаемых данных снизился у обеих страниц (хотя в данном случае эта разница почти незаметна). Но самое важное тут то, что общий объём CSS- и JS-данных Linaria-варианта страниц меньше, чем размер JS-бандла того варианта приложения, в котором используется Styled Components.

Анализ используемого кода


Если проанализировать объём используемого кода, то окажется, что в Linaria-варианте приложения имеется большой объём (около 55 Кб) неиспользуемого CSS-кода. А в приложении, где применяется Styled Components это — всего 6 Кб (причём это — CSS из npm-пакета, а не из самой библиотеки Styled Components). Размер неиспользуемого JS-кода в Linaria-варианте приложения на 20 Кб меньше, чем в Styled Components-варианте. Но общий объём неиспользуемого кода больше там, где применяется Linaria. Это — один из компромиссов, на которые приходится идти тому, кто использует внешний CSS.

Анализ используемого кода домашней страницы.

Styled Components Linaria
Размер неиспользуемого CSS 6,5Кб 55,6Кб
Размер неиспользуемого JS 932Кб 915Кб
Общий размер 938,5Кб 970,6Кб

Анализ используемого кода поисковой страницы.

Styled Components Linaria
Размер неиспользуемого CSS 6,3Кб 52,9Кб
Размер неиспользуемого JS 937Кб 912Кб
Общий размер 938,5Кб 970,6Кб

Аудит производительности в Lighthouse


Если уж мы говорим об анализе производительности — непростительно будет не взглянуть на то, что выдаёт Lighthouse. Сравнение показателей (средние значения после трёх запусков Lighthouse) можно видеть на нижеприведённых диаграммах. Тут, помимо показателей группы Web Vitals, имеются ещё два показателя — Main thread work и Execution time. Main thread work — это время парсинга, компиляции и запуска ресурсов, большая часть которого уходит на работу с JS, хотя вклад в этот показатель вносят и подготовка макета страницы, и вычисление стилей, и вывод данных, и другие процессы. Execution time — это время выполнения JS-кода. Я не включил сюда показатель Cumulative Layout Shift, так как он близок к нулю, и он выглядит практически одинаково для вариантов приложения, в котором используется Linaria и Styled Components.


Показатели Lighthouse для домашней страницы


Показатели Lighthouse для поисковой страницы

Как видите, Linaria-вариант приложения лучше, чем Styled Components-вариант, выглядит в Web Vitals-тестах (он показал худший результат лишь однажды, по показателю CLS). Иногда преимущество оказывается довольно-таки значительным. Например, на домашней странице показатель LCP оказывается лучше на 870 мс, а на поисковой странице — на 1,2 с. Страница, на которой используется обычный CSS, не только быстрее рендерится, но и требует меньше ресурсов. А время блокировки и время, необходимое на выполнение всего JS-кода, соответственно, меньше на 300 мс и примерно на 1,3 с.

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


Lighthouse может дать нам много интересных сведений о производительности приложений. Но нет лучшего средства для детального анализа производительности, чем вкладка инструментов разработчика Chrome Performance. В данном случае то, что удалось выяснить с помощью инструментов этой вкладки, согласуется с тем, что мы уже видели после выполнения Lighthouse-испытаний. Взгляните на следующие диаграммы.


Профилирование производительности домашней страницы


Профилирование производительности поисковой страницы

На страницах, при создании которых используется библиотека Styled Components, имеется больше задач, выполняющихся длительное время. И на завершение этих задач, в сравнении с Linaria-вариантом страниц, нужно больше времени.

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


Сравнение процесса загрузки разных вариантов домашней страницы

Сравнение особенностей drag-and-drop-взаимодействия со страницами


Я решил сравнить страницы не только по показателям их загрузки, но и на предмет их быстродействия при работе с ними. А именно, я измерил производительность страниц при выполнении действий, предусматривающих перетаскивание элементов и размещение их по группам. Итоговые результаты приведены ниже. Как видно, даже в этом тесте Linaria побеждает Styled Components в нескольких категориях.

Styled Components Linaria Разница
Показатель Scripting, мс 2955 2392 -563
Показатель Rendering, мс 3002 2525 -477
Показатель Painting, мс 329 313 -16
Общее время блокировки, мс 1862,66 994,07 -868


Сравнение процесса взаимодействия с разными вариантами страницы

Итоги


Вот и всё. Как видите, использование технологии CSS-in-JS, предусматривающей вычисление стилей во время работы страницы, оказывает заметное воздействие на производительность. Это актуально, в основном, для недорогих устройств и для регионов с медленными или дорогими интернет-каналами. Поэтому, возможно, нам следует ответственнее подходить к тому, какие именно инструменты мы выбираем, и к тому, как именно ими пользуемся. Отличный «опыт разработчика» не должен достигаться ценой ухудшения «пользовательского опыта».

Я полагаю, что мы (разработчики) должны больше размышлять о том, каковы последствия выбора тех или иных инструментов. Когда я в следующий раз начну работу над новым проектом — технологией CSS-in-JS я больше пользоваться не буду. Я либо применю обычный CSS, либо воспользуюсь альтернативой CSS-in-JS, библиотекой, занимающейся обработкой стилей во время сборки проекта и извлекающей стили из JS-бандлов.

Я думаю, что следующим значительным феноменом мира CSS станут CSS-in-JS-библиотеки, обрабатывающие стили во время сборки проектов. Дело в том, что появляется всё больше и больше таких библиотек (например — свежайшая vanilla-extract от Seek). Да и крупные компании тоже двигаются в этом направлении, например — Facebook.

Как вы относитесь к CSS-in-JS?