Всем привет! Меня зовут Всеволод Золотов, я Senior Frontend в компании Bimeister.
CSS-in-JS очень быстро набрал популярность в React-комьюнити, но насколько актуален этот подход в Angular? В данной статье сравним удобство разработки и производительность двух визуально идентичных приложений (time-tracker), написанных с использованием SASS и @emotion/css.
Немного предыстории
За время существования веб-разработки, пожалуй, самым популярным подходом к стилизации стало использование препроцессора SASS (есть и другие, но SASS – самый популярный, будем рассматривать его). SASS – очень мощный и зарекомендовавший себя инструмент, который позволяет увеличить уровень абстракции CSS-кода. Например, в Angular Material вся стилизация и темизация, по большей части, реализована с помощью SASS. Используя большой набор конструкций, предлагаемых SASS, невольно задаешься вопросом: «Есть ли альтернативные варианты стилизации приложений?».
Конечно есть! Одной из популярных альтернатив в настоящее время является CSS-in-JS (согласно State of CSS). Большинство разработчиков использует менее 30% возможностей препроцессоров и вряд ли когда-то слышали про maps, lists и другие конструкции. Почему бы не дать возможность разработчикам использовать мощь CSS внутри JavaScript?
CSS-in-JS очень быстро набрал популярность в React-комьюнити. Все потому, что React-разработчики повторяют годами одну и ту же мантру – «Все будет JavaScript». Поэтому большинство CSS-in-JS решений изначально разрабатывались для React-а. Но есть и framework-agnostic библиотеки, одна из таких и была использована для сравнения с SASS, – @emotion/css.
Скрещиваем CSS-in-JS с Angular
Дисклеймер: Есть огромное множество статей, сравнивающих CSS-in-JS и CSS, поэтому не ждите от данного материала перечисления всех возможных проблем и теоретической части. В данной работе было произведено сравнение двух подходов на практике.
CSS-in-JS или все-таки CSS-in-CSS?
“Пишите CSS-in-CSS” – говорили они, и, конечно, были правы. Так все и делали, пока не появился компонентный подход.
Ключевая проблема CSS в мире компонентов заключается в том, что он изначально разрабатывался для мира страниц. В CSS только одно глобальное пространство имен и нет доступа к состояниям компонентов.
Необходимо отметить, что фреймворки с компонентным подходом решили эти, и не только эти, проблемы CSS по-разному. Стилизация стала обходиться меньшей болью. В Angular стили изолируются на уровне компонента, во Vue есть scoped styles, в React – CSS modules. Но CSS так и не получил простого доступа к состоянию компонентов.
А нужен ли CSS доступ к состоянию компонентов?
Возможно, не нужен. У данной статьи нет цели продать подход CSS-in-JS, ведь можно обойтись классами-модификаторами, а при необходимости динамически менять стили. Будем использовать inline styles. Но CSS-in-JS позволяет вам унифицировать подход к стилизации и забыть про inline styles, модификаторы и т. п.
Как работает @emotion/css?
Чтобы понять, как на самом деле работает @emotion/css потребовалось ознакомиться с исходным кодом.
Библиотека воспринимает SASS/SCSS-подобный код, но только селекторы – никаких миксинов и подобных конструкций. Принцип работы @emotion/css описывать словами нет смысла, поэтому предлагаю взглянуть на упрощенную блок-схему.
Стоит отметить, что если на вход в функцию css()
попадет уже ранее обработанный код, то благодаря кэшированию в тег <head>
новых тегов <style>
добавлено не будет, и функция вернет уже известный CSS-класс. Поэтому при работе с @emotion/css советуем выделять статичные и динамические стили в отдельные классы – так вы повысите производительность приложения и сэкономите на отладке.
Что разрабатывали?
Было решено написать простое приложение, но с использованием разных подходов к стилизации. В итоге получили два одинаковых приложения для учета времени, написанных с использованием SASS и CSS-in-JS.
Какие были различия в написании кода?
Файловая структура
Благодаря CSS-in-JS вы можете писать стили прямо внутри компонента, в одном файле, вместе с логикой, но тогда логика компонента теряется среди стилей. Поэтому было принято решение выделить отдельный файл some-classes.class.ts с CSS-классами. Так что с точки зрения разделения логики, шаблона и стилей почти ничего не изменилось.
CSS-in-JS
├── button.component.html
├── button.component.ts
└── button-classes.class.ts
SASS
├── button.component.html
├── button.component.ts
└── button.component.scss
Конфигурация темы
Для конфигурации темы в подходе с CSS-in-JS был использован файл с конфигурацией и сервис (ThemeService) для более удобного получения необходимого цвета.
@Injectable({
providedIn: 'root'
})
export class ThemeService {
constructor(
@Inject(THEME_CONFIG_TOKEN) private readonly themeConfig: ThemeConfig
) {}
public getColor(path: ColorPath): string {
if (typeof path === 'string') {
return this.themeConfig.colors[path][0];
}
if (Array.isArray(path) && path.length === 1) {
return this.themeConfig.colors[path[0]][0];
}
return this.themeConfig.colors[path[0]][path[1]];
}
}
В подходе с SASS файл заменил файл с sass:map
, а сервис – функцию-хелпер get-map-field.
@use 'sass:map';
@function get-map-field($map, $field-path) {
$result: $map;
@each $field in $field-path {
@if (type-of($result) != 'map') {
@error "Field '#{$field}' in #{$field-path} does not exist in" $map;
}
$result: map.get($result, $field);
}
@return $result;
}
Файлы стилизации
В подходе с CSS-in-JS вместо селекторов используются свойства, геттеры и методы класса. Плюсом появляется возможность обращения к сервисам и другим сущностям, переданным в конструктор класса.
export class ControlBarClasses {
constructor(private readonly themeService: ThemeService) {}
public readonly root: string = css`
display: flex;
justify-content: space-between;
align-items: center;
color: ${this.themeService.getColor(['light', 100])};
`;
}
В подходе с SASS все просто используем функцию get-map-field.
@use 'src/styles/variables' as *;
@use 'src/styles/functions' as *;
.control-bar {
display: flex;
justify-content: space-between;
align-items: center;
color: get-map-field($colors, [light, 100]);
}
Файлы шаблонов
В подходе с CSS-in-JS вместо строки передается переменная с CSS-классом.
<div [class]="classes.root">
<div>Total time: {{ totalTimeSpent$ | async | date: "H'h' m'm' s's'" }}</div>
<app-time-runner (stateChange)="handleStateChange($event)"></app-time-runner>
</div>
Неоднозначное поведение
В случае с использованием SASS, в файле стилей можно использовать селектор :root
, тогда как в CSS-in-JS делать так нельзя. Поэтому данная проблема была решена за счет использования декоратора @HostBinding.
@HostBinding('class')
public readonly hostClass: string = this.classes.host;
Если класс host-элемента должен быть динамическим, то использовать будем Renderer2
, или markForCheck
после обновления значения класса.
this.isHover$.subscribe((isHover: boolean) => {
const hostElement: HTMLElement = this.viewRef.element.nativeElement;
if (isHover) {
this.renderer.addClass(hostElement, this.classes.highlightHost);
return;
}
this.renderer.removeClass(hostElement, this.classes.highlightHost);
});
Как сравнивали производительность подходов?
Сравнивали в первую очередь по всем известным метрикам, таким, как: First Paint, First Contentful Paint, Largest Contentful Paint, но также добавили в оценку события DOMContentLoaded, OnLoad. Вдобавок оценили размер бандла и скорость сборки.
За счет встроенных в chrome dev tools средств замедляли как процессор, так и скорость соединения для проведения сравнения в разных условиях.
В компьютерной версии проверять было бессмысленно – Lighthouse показывал 100% по производительности, поэтому сразу перешли к мобильным устройствам.
Начнем с “малого”. Размер бандла и скорость сборки
SASS – неоспоримый победитель, но размер @emotion/css в бандле составляет 34,42 кб, поэтому метрики неоднозначны. Чем больше будет проект, тем менее заметна будет разница. То же можно сказать и про скорость билда.
Метрика | SASS | CSS-in-JS | Победитель | Разница |
Build time, ms | 2896 | 3380 | SASS | 16,71% |
Build size, kb | 300,51 | 317,93 | SASS | 5,8% |
Lighthouse Performance
Lighthouse дал уж очень плохую оценку для столь небольшого приложения. После поиска путей увеличения оценки было решено сделать gzip билда. Теперь наблюдаем довольно интересную картину. Если не сжимать билд, то CSS-in-JS лидирует на целых 7,67%, что является довольно внушительным результатом. Но если файлы сжимать, то преимущество нивелируется.
Метрика | SASS | CSS-in-JS | Победитель | Разница |
Performance, % | 78,2 | 84,2 | CSS-in-JS | 7,67% |
Performance gzip-build, % | 92,8 | 92,6 | SASS | 0,22% |
DCL, FP, FCP, LCP, OL.
Знакомые всем метрики и события были измерены при различных состояниях CPU и Network. Также стоит учесть, что файлы билда не сжимались, а функция кэширования запросов была отключена.
Приложение не выполняет запросов и сторонних операций до рендеринга, поэтому метрики FP, FCP, LCP совпали (далее будем рассматривать их вместе).
Как видим из графиков, при нормальных условиях разницы между подходами практически нет – CSS-in-JS немного выигрывает. Подход с SASS чувствителен к скорости интернета из-за того, что файл стилей догружается после js-составляющей бандла. А CSS-in-JS, наоборот, чувствителен к производительности процессора, так как стили генерируются и добавляются в тег <head>
в runtime. Но в случае с низкой производительностью и низкой скоростью интернета, выигрывает CSS-in-JS.
Выигрыш в скорости загрузки приложения
На данном графике можете наблюдать, на сколько процентов выигрывает или проигрывает в скорости загрузки приложения подход с CSS-in-JS по сравнению с SASS. Если взять все случаи и посчитать, на сколько процентов выигрывает CSS-in-JS в среднем, получим 3,4%. Стоит также учесть, что при включенном кэшировании запросов выигрыш CSS-in-JS будет практически незаметен, так как стили все также будут заново генерироваться при перезагрузке страницы.
Заключение
В теории CSS-in-JS – средство, с помощью которого можно унифицировать подход к разработке, но на практике этого не происходит. В SASS не приходится думать о производительности, так как стили скомпилированы еще в build-time. А уменьшенное время загрузки благодаря компиляции стилей в runtime нивелируется сжатием файлов билда и кэшированием файлов в браузере. CSS-in-JS не панацея и не волшебная пуля, которая может что-то улучшить в вашей кодовой базе. В мире Angular и Vue данный подход не пользуется популярностью, так как это все-таки html-first фреймворки и в них принято разделять отображение, стилизацию и логику. Но в мире React CSS-in-JS становится все популярнее. И стоит отметить, что на данный момент CSS-in-JS движение стремится к компиляции в CSS в build-time.
Исходники: https://github.com/vsezol/angular-css-methodologies