Как мы отказались от styled-components в React Native приложениях
Привет! Меня зовут Виталик, я Тимлид команды UI-kit в Профи.
Styled-components является стандартом написания стилей для многих команд, которые разрабатывают приложения на React Native. Но мы не всегда задумываемся, зачем мы тащим это в продукт и какую выгоду получим. А что если от styled-components больше вреда, чем пользы? Я поделюсь нашим опытом в Профи и попробуем разобраться вместе.
Какие проблемы
У Профи есть 2 мобильных приложения на React Native, которые написаны с использованием styled-components. Для этих приложений существует одна дизайн-система и UI-kit.
Мы накопили несколько проблем, связанных с UI, во всех этих продуктах:
Проблема | В чем проблема |
1. Нет однозначной концепции к написанию стилей | Где-то писали styled-компоненты, где-то прописывали инлайн-стили, а где-то создавали StyleSheet-объекты. |
2. Нет нормальной токенизации и темизации | У нас есть дизайн-система и набор токенов для цвета, радиусов, шрифтов и т.д. При этом в компонентах мы просто импортируем токены и указываем в стилях:
Проблема здесь в том, что у нас нет реактивности. То есть, в случае появления темной темы, наши цвета не обновятся автоматически при переключении темы. Нужен какой-то Provider с токенами. |
3. Медленный рендер | Некоторые экраны с большим количеством компонентов отрисовываются слишком медленно. Это аффектит пользователей. |
Мы взялись за исследование этих проблем. У нас было предположение, что именно styled-components сильно замедляет рендер. При этом скорость UI является одной из ключевых метрик для продукта. В этот момент мы начали изучать возможность отказа от styled-components. Стали рассматривать другие варианты стилевых фреймворков, которые заодно решили бы и другие проблемы.
Варианты подходов
Оставить styled-components
Проблему с производительностью не решить, но можно отдельно решить другие 2 проблемы.Добавить styled-system
Styled-system — это дополнение к styled-components. Библиотека позволяет строить UI с использованием специальных атрибутов, которые формируются на основе токенов дизайн-системы. Опять же, производительность лучше не станет, но можно было элегантно решить другие проблемы.Перейти на Restyle
Restyle — это библиотека для RN. Она помогает выстраивать UI, используя специальные атрибуты. Атрибуты формируются на основе токенов дизайн-системы. Основное отличие от styled-system в том, что это библиотека не использует styled-components под капотом. Такой подход решил бы все проблемы.Перейти на Tailwind
Tailwind — это технология для веба, но есть и решения для React Native. Все стили создаются через предопределенные классы на все случаи. Дальше вы увидите пример как это выглядит в коде. Подразумевалось, что производительность будет лучше за счет кэширования стилей. Такой подход решил бы все проблемы.Использовать нативный StyleSheet
В этом варианте мы будем использовать нативный инструмент StyleSheet.create для создания стилей без дополнительных библиотек. Таким образом можно решить все проблемы.
Замеры скорости рендера
Для начала мы произвели замеры скорости всех вариантов. Нам нужно было подтвердить гипотезу, что styled-components действительно замедляет рендер приложения. Заодно мы получили важные данные о скорости рендера других вариантов. Это поможет нам при выборе целевого подхода.
Для теста создали компонент в пяти вариантах. Компонент внешне получился не очень естественный, но для нас важнее было сделать максимально тяжелый и репрезентативный компонент:
он содержит 100 строк,
в каждой строке есть номер, созданный шрифтом из ДС,
в каждой строке 20 блоков, с радиусом из ДС и рандомным цветом из ДС.
Проводилось 10 замеров рендера для каждого варианта, затем для результата бралось среднее значение.
Измерялось время рендера следующим образом:
export const Component = () => {
const start = new Date().getTime();
useEffect(() => {
const renderTime = new Date().getTime() - start;
console.log(renderTime);
}, []);
return (
...
);
};
Вариант без дополнительных библиотек оказался самым быстрым — рендер компонента занял всего 408 ms. Второй по скорости вариант — это Restyle.
Styled-components действительно сильно замедляет рендер UI. Время отрисовки компонента заняло 632 ms. Мы подтвердили нашу гипотезу. Styled-system еще больше усугубляет ситуацию и увеличивает время рендера, потому что создает дополнительный слой абстракции на токены.
Скорость интерфейса в нашем приложении очень важна. А styled-components уже ничего не могло спасти. Поэтому уже в этой точке мы приняли решение отказываться от вариантов со styled-components.
❌ Styled-components
❌ Styled-system
У нас осталось 3 варианта на выбор: Restyle, Tailwind или StyleSheet.
Примеры кода Restyle & Tailwind & StyleSheet
Рассмотрим пример каждого подхода, чтобы понять, как он будет выглядеть стилистически. Стиль и сложность кода — один из важных критериев для выбора подхода.
Код был составлен для типичного компонента из приложения:
отступы из ДС, кратные 4px,
радиус из ДС,
несколько вариантов шрифта из ДС,
пример компонента Button из UI-kit,
цвет текста и бэкграунд из ДС.
Ниже в спойлерах можно посмотреть примеры кода каждого варианта.
Restyle
import {Box, Text, Button} from '@profi/uikit-rn';
const RestyleExample = ({ onPress }) => {
return (
<Box bg="g300" p={4} borderRadius="xl">
<Text mb={5} color="g500" variant="headingL">
Найдем любого профи - просто создайте заказ
</Text>
<Button onPress={onPress} bg="profiBrand">
<Text color="g100" variant="bodyM">Создать заказ</Text>
</Button>
</Box>
);
};
Tailwind
import {Text, View} from 'react-native';
import {Button} from '@profi/uikit-rn';
mport {useTailwind} from 'tailwind-rn';
const TailwindExample = ({ onPress }) => {
const tw = useTailwind();
return (
<View style={tw('bg-g-300 p-4 rounded-xl')}>
<Text style={tw('mb-4 color-g-500 text-heading-l')}>
Найдем любого профи - просто создайте заказ
</Text>
<Button onPress={onPress} color="profiBrand">
<Text style={tw('text-body-m')}>Создать заказ</Text>
</Button>
</View>
);
};
StyleSheet
import {Text, View} from 'react-native';
import {Heading, Typography, Button} from '@profi/uikit-rn';
import {TOKENS} from 'tokens';
const StyleSheetExample = ({ onPress }) => {
return (
<View style={styles.container}>
<Heading size="l" style={styles.titleText}>
Найдем любого профи - просто создайте заказ
</Heading>
<Button onPress={onPress} color="profiBrand">
<Typography size="m" style={styles.buttonText}>Создать заказ</Typography>
</Button>
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: TOKENS.colors.g300,
borderRadius: ${TOKENS.radiusM.xl},
padding: 16px;
},
titleText: {
marginBottom: 20,
color: TOKENS.colors.g500,
},
buttonText: {
color: TOKENS.colors.g100,
},
});
Что здесь сразу же бросается в глаза — Restyle и Tailwind выглядят очень похоже стилистически. В Restyle стили указываются атрибутами, а в Tailwind — классами. Но даже сами названия выглядят похоже.
Tailwind VS Restyle
В этом моменте у нас возникло разногласие. Часть разработчиков была категорически против того, чтобы писать стили атрибутами. Часть была против того, чтобы писать классы.
Почему атрибуты — это плохо | Почему классы — это плохо |
Атрибуты стилей перемешиваются с другими пропсами. Тяжело отделять визуальную часть от бизнес-логики | В RN нет классов. Это накрученная абстракция над элементами. К тому же длинную строку с классами тяжело считывать и понимать |
Мы провели опрос среди разработчиков. В опросе был прикреплен вариант с кодом Restyle и Tailwind. И задан следующий вопрос:
Какой стилевой фреймворк тебе нравится больше – Restyle или Tailwind? Оцени каждый вариант от 1 до 5, где 1 – это очень плохо, а 5 – просто супер. Оценивай по личным критериям (читаемость, легкость написания, масштабируемость)
По результатам опроса поняли, что Tailwind вариант почти никому не нравится стилистически. К тому же он создавал дополнительные проблемы в инфраструктуре, так как требовал компиляции CSS. А еще Tailwind медленнее чем Restyle.
Отмели вариант с Tailwind:
❌ Tailwind
И у нас осталось на выбора всего 2 варианта: Restyle или StyleSheet.
Restyle VS StyleSheet
Пришло время вспомнить наши изначальные проблемы, ради чего все начиналось. Каждый вариант решает проблемы по-своему:
Проблема | StyleSheet | Restyle |
1. Нет концепции | Мы сами пишем простой фреймворк под наши задачи, описываем правила | Restyle дает свод правил из коробки |
2. Нет токенизации и темизации | Мы создаем контекст и дополнительную функцию обертку над StyleSheet.create() , в которой доступны токены | Контекст, хуки для токенов и сами токены доступны из коробки |
3. Медленный рендер | StyleSheet не использует никаких дополнительных парсеров стилей и абстракций. Такой метод самый быстрый | Restyle работает быстрее чем styled-components и любые другие сторонние библиотеки. Но Restyle медленнее, чем StyleSheet |
Здесь мы поняли, что StyleSheet без дополнительных оберток не решит вторую проблему. А если доработать этот вариант, то скорость рендера может увеличиться. Ведь мы делали замеры без всяких оберток.
Дальше в таблице вы увидите уже доработанный вариант StyleSheet, а также обновленные замеры скорости рендера.
Финал
Мы сделали последнюю таблицу сравнений этих подходов, чтобы принять финальное решение:
StyleSheet | Restyle | |
Скорость первого рендера | 375 ms | 404 ms |
Библиотеки | - | |
Возможность оптимизации / контролируемость | При необходимости можем постоянно оптимизировать наше решение: - можем не вызывать контекст темы, если он не нужен, а воспользоваться обычным StyleSheet - для длинных списков можно не вызывать контекст в каждом элементе списка, а прокинуть один раз, достав предварительно из хука нужный токен | Мы имеем архитектуру из коробки и не можем добавлять свои кастомизации или каким-то образом оптимизировать библиотеку. |
Работа с брейкпоинтами | Можно достать брейкпоинты в createStyleSheet из темы:
| Есть встроенные инструменты – хук useResponsiveProp() или специальный короткий формат для пропсов:
|
Работа со стилями | Стили всегда лежат отдельно от компонента | Стили прописываются в атрибутах |
Миграция | - изменить сам компонент. - переписать styled-components стили на StyleSheet с функцией оберткой | - переписать JSX и все стили перенести в компонент. Все элементы заменить на Box, Text - отдельно обработать кейсы, где блок, отличный от View |
Сложность разработки | - разобраться в кастомном решении (прочитать нашу документацию) - привыкнуть оборачивать стили функцией и доставать все токены из хука (цвета, радиусы, отступы) | - прочитать документацию по Restyle - привыкнуть к новому формату написания стилей через атрибуты + привыкнуть к неймингу атрибутов (m, p, mt, bg и т.д) |
Пример кода для StyleSheet
import {Text, View} from 'react-native';
import {
Heading,
Typography,
Button,
createStyleSheet,
useStyleSheet
} from '@profi/uikit-rn';
const StyleSheetExample = ({ onPress }) => {
const styles = useStyleSheet(stylesheet);
return (
<View style={styles.container}>
<Heading size="l" style={styles.titleText}>
Найдем любого профи - просто создайте заказ
</Heading>
<Button onPress={onPress} color="profiBrand">
<Typography size="m" style={styles.buttonText}>
Создать заказ
</Typography>
</Button>
</View>
);
};
const stylesheet = createStyleSheet(
theme => {
return StyleSheet.create({
container: {
backgroundColor: theme.colors.g300,
borderRadius: theme.radius.xl,
padding: theme.space[4],
},
titleText: {
marginBottom: theme.spaces[5],
color: theme.colors.g500,
},
buttonText: {
color: theme.colors.g100,
},
});
},
);
Пример кода для Restyle
import {
Box,
Text,
Button
} from '@profi/uikit-rn';
const RestyleExample = ({ onPress }) => {
return (
<Box bg="g300" p={4} borderRadius="xl">
<Text mb={5} color="g500" variant="headingL">
Найдем любого профи - просто создайте заказ
</Text>
<Button onPress={onPress} bg="profiBrand">
<Text color="g100" variant="bodyM">
Создать заказ
</Text>
</Button>
</Box>
);
};
Решение
Мы решили выбрать вариант со StyleSheet по следующим причинам:
Решение простое и не требует больших трудозатрат на внедрение.
Этот вариант работает быстрее Restyle.
Будет гораздо легче переписать старые компоненты на этот вариант и полностью убрать styled-components из кода.
Мы можем дорабатывать это решение по собственным требованиям.
Такой вариант привычнее для разработки. Концепция кардинально не меняется (стили отдельно).
Моменты, которые смущали:
Нужно тратить дополнительные усилия, чтобы научить всех использовать наше решение правильно (писать дополнительные документации, проводить обучающие митапы).
Код выглядит слегка раздутым по сравнению с Restyle.
Выводы
По моим ощущениям, вариант со StyleSheet победил, потому что у нас уже написаны приложения. И переход со styled-components на StyleSheet кажется не таким болезненным, потому что концепция сильно не меняется.
При этом Restyle очень классно бы зашел для новых приложений, где такая архитектура будет закладываться изначально. Особенно, если разработчиков не смущают стили в атрибутах.
Но мы сошлись в одном, без всяких сомнений. Если ваше RN-приложение сложнее нескольких экранов, то styled-components — это вредная библиотека. И стоит от нее избавляться как можно раньше.