Повышение производительности React и Redux с Reselect

Перевод статьи Neil Fenton: Improving React and Redux performance with Reselect.

При совместном использовании React и Redux представляют собой потрясающую комбинацию технологий, помогающих нам структурировать приложения с настоящим разделением задач. Даже при том, что React чрезвычайно эффективен из коробки, наступает время, когда требуется еще большая производительность.

Одной из наиболее дорогостоящих операций, которые может выполнять React, является цикл рендеринга. Он запускается, когда компонент обнаруживает изменение входных данных.

Когда мы впервые начинаем работу с React, мы обычно не беспокоимся о том, насколько затратны циклы рендеринга. Но по мере усложнения наших пользовательских интерфейсов нам требуется об этом задумываться. React предлагает нам некоторые инструменты для захвата цикла рендеринга и предотвращения повторной перерисовки, если мы сочтем, что в этом нет необходимости. Для этого мы можем воспользоваться событием жизненного цикла — componentShouldUpdate, которое возвращает boolean, отвечающий за то, будет ли компонент обновлен или нет. Это основа PureRenderMixin, который сравнивает входящие свойства (props) и состояние (state) с предыдущими свойствами и состоянием и возвращает false, если они равны.

Это, в сочетании с неизменяемыми наборами данных, дает нам существенное улучшение производительности, поскольку мы можем легко определить, должен ли компонент повторно отрисовываться или нет. К сожалению, и этого не достаточно.

Рассмотрим следующую проблему. Мы создаем корзину покупок с тремя типами входящих данных:

  • Товары в корзине
  • Количество товаров
  • Налог (зависящий от региона)

Проблема состоит в том, что всякий раз, когда изменяется состояние любого из пунктов (добавляется новый элемент, изменяется количество или изменяется состояние выбора), все нужно будет пересчитать и повторно отрисовать. Вы можете увидеть, как это будет проблематично, если в нашей корзине есть сотни предметов. Изменение процента налога приведет к пересчету позиций в корзине, но не должно. Процент налога — это просто изменение в полученных данных. Только общая сумма и общая сумма налога должны меняться и запускать последующую перерисовку. Давайте посмотрим, как мы можем исправить эти проблемы.

Решение — reselect

Reselect — это библиотека для создания мемоизированных селекторов (memoized selectors). Мы определяем селекторы как функции, извлекающие фрагменты состояния Redux для наших компонентов React. Используя мемоизацию, мы можем предотвратить ненужные перерисовки и пересчеты полученных данных, что, в свою очередь, ускорит наше приложение.

Рассмотрим следующий пример:

Если бы у нас было несколько сотен или тысяч вещей, перерисовка всех предметов в нашей корзине была бы дорогостоящей, даже если бы менялся только процент налога. А если бы мы реализовали поиск? Должны ли мы повторно пересчитывать все элементы и налоги каждый раз, когда пользователь ищет что-то в корзине? Мы можем предотвратить эти дорогостоящие операции, перемещая их использование в мемоизированные селекторы. При использовании мемоизированных селекторов, если дерево состояний велико, нам не нужно беспокоиться о том, что дорогие вычисления выполняются каждый раз при изменении состояния. Мы также можем добавить дополнительную гибкость для нашего интерфейса, разбив их на отдельные компоненты.

Давайте посмотрим на простой селектор, используя Reselect:

В приведенном выше примере, мы разбили нашу функцию поиска товаров в корзине на две функции. Первая функция (строка 3) просто получит все элементы в корзине, а вторая функция является мемоизированным селектором. Reselect предоставляет createSelector API, позволяющий нам создать мемоизированный селектор. Это означает, что getItemsWithTotals будет вычисляться при первом запуске функции. Если эта же функция вызывается снова, но входные данные (результат getItems) не изменились, функция просто вернет кешированный расчет элементов и их итогов. Если элементы изменены (например, добавлен элемент, изменилось количество, любые манипуляции с результатом getItems), функция снова будет выполнена.

Это мощная концепция, позволяющая нам полностью оптимизировать те компоненты, которые должны быть перерисованы, и когда их производное состояние должно быть пересчитано. Это означает, что нам больше не нужно беспокоиться о getItems: общая стоимость каждого элемента начинает рассчитываться, когда операции не зависят от изменений состояния.

Мы можем продолжить эту тенденцию, создав селектора для всех наших полученных данных. Это включает в себя расчет промежуточного итога, общий расчет налога и итоговую сумму:

Использование селектора

Давайте посмотрим как можно воспользоваться нашими селекторами на примере селектора getItemsWithTotals в одном из наших компонетов.

Теперь у нас есть компонент, знающий только про элементы в корзине. Это хороший подход, поскольку он не затрагивает итоговую сумму, налоги и так далее. Хотя это не самый полезный компонент для повторного использования, это очень производительный компонент. Изменения, которые он не затрагивает (например, изменения в исчислении налога), не будут вызывать дополнительную перерисовку.

Применение этого подхода к остальной части корзины означает, что у нас будет компонент, отвечающий за отображение промежуточного итога, общего и налогового расчета.

Создание этих оптимизаций на ранней стадии разработки вашего приложения избавит от работы в будущем, когда вам нужно будет исправлять проблемы с производительностью. Я рекомендую перейти на использование Reselect как можно скорее. Одно из главных преимуществ выноса селекторов из наших компонентов означает, что мы можем легко протестировать эти производные вычисления данных, как и любую другую функцию JavaScript. Мы просто мокаем (mock) наше состояние Redux, а затем проверяем ожидаемый результат на основе предоставленного состояния.

Для дальнейшей демонстрации этих концепций обратитесь к демо.


Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Ссылка на GitHub