Привет! Меня зовут Виталик, я Тимлид команды UI-kit в Профи.

Styled-components является стандартом написания стилей для многих команд, которые разрабатывают приложения на React Native. Но мы не всегда задумываемся, зачем мы тащим это в продукт и какую выгоду получим. А что если от styled-components больше вреда, чем пользы? Я поделюсь нашим опытом в Профи и попробуем разобраться вместе.

Какие проблемы

У Профи есть 2 мобильных приложения на React Native, которые написаны с использованием styled-components. Для этих приложений существует одна дизайн-система и UI-kit. 

Мы накопили несколько проблем, связанных с UI, во всех этих продуктах:

Проблема

В чем проблема

1. Нет однозначной концепции к написанию стилей

Где-то писали styled-компоненты, где-то прописывали инлайн-стили, а где-то создавали StyleSheet-объекты.

2. Нет нормальной токенизации и темизации

У нас есть дизайн-система и набор токенов для цвета, радиусов, шрифтов и т.д. При этом в компонентах мы просто импортируем токены и указываем в стилях:

background-color: ${tokens.color.g100};

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

3. Медленный рендер

Некоторые экраны с большим количеством компонентов отрисовываются слишком медленно. Это аффектит пользователей.

Мы взялись за исследование этих проблем. У нас было предположение, что именно styled-components сильно замедляет рендер. При этом скорость UI является одной из ключевых метрик для продукта. В этот момент мы начали изучать возможность отказа от styled-components. Стали рассматривать другие варианты стилевых фреймворков, которые заодно решили бы и другие проблемы.

Варианты подходов

  1. Оставить styled-components
    Проблему с производительностью не решить, но можно отдельно решить другие 2 проблемы.

  2. Добавить styled-system
    Styled-system — это дополнение к styled-components. Библиотека позволяет строить UI с использованием специальных атрибутов, которые формируются на основе токенов дизайн-системы. Опять же, производительность лучше не станет, но можно было элегантно решить другие проблемы.

  3. Перейти на Restyle
    Restyle — это библиотека для RN. Она помогает выстраивать UI, используя специальные атрибуты. Атрибуты формируются на основе токенов дизайн-системы. Основное отличие от styled-system в том, что это библиотека не использует styled-components под капотом. Такой подход решил бы все проблемы.

  4. Перейти на Tailwind
    Tailwind — это технология для веба, но есть и решения для React Native. Все стили создаются через предопределенные классы на все случаи. Дальше вы увидите пример как это выглядит в коде. Подразумевалось, что производительность будет лучше за счет кэширования стилей. Такой подход решил бы все проблемы.

  5. Использовать нативный 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
(время немного больше за счет дополнительных оберток для каждого блока и выфильтровывания пропсов и мапинга стилей)

Библиотеки

@shopify/restyle

Возможность оптимизации / контролируемость

При необходимости можем постоянно оптимизировать наше решение:

- можем не вызывать контекст темы, если он не нужен, а воспользоваться обычным StyleSheet

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

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

Работа с брейкпоинтами

Можно достать брейкпоинты в createStyleSheet из темы:

const stylesheet = createStyleSheet(
  (theme, breakpoint) => {
   return StyleSheet.create({
      item: {
          padding: theme.spaces[breakpoint === ‘s’ ? 4 : 5]
        },
    });
  },
);

Есть встроенные инструменты – хук useResponsiveProp() или специальный короткий формат для пропсов:

<Box marginTop={{ s: 4, m: 5 }} />

Работа со стилями

Стили всегда лежат отдельно от компонента

Стили прописываются в атрибутах

Миграция

- изменить сам компонент.
Было: <Container />
Станет: <View style={styles.container} />

- переписать 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 — это вредная библиотека. И стоит от нее избавляться как можно раньше.

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


  1. ALapinskas
    22.11.2023 08:42
    -1

    Для токенов css препроцессоры существуют.


  1. fransua
    22.11.2023 08:42
    -1

    А Вы не рассматривали вариант использования css-variables которые меняются в зависимости от темы и media queries в обычных css-module или даже просто css/less? Или этот подход не работает в React Native?


    1. vital_pavlenko Автор
      22.11.2023 08:42
      +1

      В React Native нет CSS. Есть StyleSheet абстракция, которая просто по синтаксису похожа на CSS


      1. fransua
        22.11.2023 08:42

        Ага, понятно. Спасибо


  1. SergoMorello
    22.11.2023 08:42
    +4

    В начале статьи так и думал что в итоге падёт выбор на стандартные RN стили)

    Искренне не понимаю чем всех так манит этот стайлед-компонентс, немного бомбит даже,

    если речь не о RN а о обычном реакте, чем плох нативный механизм css модулей, всё логично, код разделён на файл стилей и файл компонента, зачем этот геморой в виде прослойки в виде стайлед-компонентс?