Не очень очевидно, как оптимизировать реагирующие приложения, использующие Redux. Но на самом деле это довольно просто. Вот краткое руководство с несколькими примерами.

При оптимизации приложений, использующих Redux с помощью response, я часто слышу, как люди говорят, что Redux работает медленно. В 99% случаев причина плохой производительности (это касается любого другого фреймворка) связана с ненужным рендерингом, поскольку обновления DOM дороги! В этой статье вы узнаете, как избежать ненужного рендеринга при использовании привязок Redux для реакции.

Как правило, чтобы обновлять компоненты реакции всякий раз, когда обновляется ваш магазин Redux, мы используем connect компонент более высокого порядка из официальных привязок реакции для Redux. Это функция, которая объединяет ваш компонент в другой компонент, который подписывается на изменения в хранилище Redux и визуализирует себя и, следовательно, своих потомков при каждом обновлении.

Быстрое погружение в react-redux, официальные привязки React для Redux

Компонент connect более высокого порядка фактически уже оптимизирован. Чтобы понять, как это лучше всего использовать, лучше понять, как это работает!

Redux, как и react-redux, на самом деле довольно маленькие библиотеки, поэтому исходный код непонятен. Я призываю людей прочитать исходный код или хотя бы его фрагменты. Если вы хотите пойти дальше, напишите свою собственную реализацию, она даст вам полное представление о том, почему библиотека спроектирована именно так.

Без лишних слов, давайте немного погрузимся в то, как работают привязки реакции. Как мы установили, центральная часть привязок реакции - это connect компонент более высокого порядка, это его сигнатура:

return function connect(
  mapStateToProps,
  mapDispatchToProps,
  mergeProps,
  {
    pure = true,
    areStatesEqual = strictEqual,
    areOwnPropsEqual = shallowEqual,
    areStatePropsEqual = shallowEqual,
    areMergedPropsEqual = shallowEqual,
    ...extraOptions
  } = {}
) {
...
}

В качестве примечания: единственный обязательный аргумент - mapStateToProps, и в большинстве случаев вам понадобятся только первые два аргумента. Однако здесь я использую подпись, чтобы проиллюстрировать, как работают привязки реакции.

Все аргументы, переданные в функцию connect, используются для генерации объекта, который передается в завернутый компонент в качестве свойств. mapStateToProps используется для отображения состояния вашего хранилища Redux на объект, mapDispatchToProps используется для создания объекта, содержащего функции - обычно эти функции являются создателями действий. Наконец, mergeProps принимает три аргумента stateProps, dispatchProps и ownProps. Первый - результат mapStateToProps, второй - результат mapDispatchToProps, а третий аргумент - это объект props, унаследованный от самого компонента. По умолчанию mergeProps просто объединяет эти аргументы в один объект, но если вы передадите функцию для аргумента mergeProps, connect вместо этого будет использовать это для генерации свойств для обернутого компонента.

Четвертый аргумент функции connect - это объект параметров. Он содержит 5 параметров: pure, которые могут быть истинными или ложными, а также 4 функции (которые должны возвращать логическое значение), которые определяют, следует ли повторно визуализировать компонент или нет. pure по умолчанию имеет значение true. Если установлено значение false, connect hoc пропустит любые оптимизации, и 4 функции в объекте параметров не будут иметь значения. Я лично не могу придумать для этого варианта использования, но возможность установить для него значение false доступна, если вы предпочитаете отключить оптимизацию.

Объект, созданный нашей функцией mergeProps, сравнивается с последним объектом props. Если наш connect HOC считает, что объект props изменился, компонент будет повторно визуализирован. Чтобы понять, как библиотека определяет, было ли изменение, мы можем взглянуть на shallowEqual функцию. Если функция вернет true, компонент не будет повторно отрисован, если он вернет false, он произведет повторный рендеринг. shallowEqual выполняет это сравнение. Ниже вы увидите часть метода shallowEqual, в котором рассказывается все, что вам нужно знать:

for (let i = 0; i < keysA.length; i++) {
  if (!hasOwn.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])) {
    return false
  }
}

Подводя итог, приведенный выше код делает следующее:

Он перебирает ключи в объекте a и проверяет, владеет ли объект B тем же свойством. Затем он проверяет, совпадает ли свойство (с тем же именем) объекта A со свойством объекта B. Если только одно из сравнений возвращает false, объекты будут считаться неравными и произойдет повторный рендеринг.

Это приводит нас к одному золотому правилу:

Дайте вашему компоненту только те данные, которые ему нужны для рендеринга.

Это довольно расплывчато, поэтому давайте рассмотрим несколько практических примеров.

Разделите подключенные компоненты

Я видел, как люди так поступали. Подписка компонента контейнера на группу состояний и передача всего через props.

const BigComponent = ({ a, b, c, d }) => (
  <div>
    <CompA a={a} />
    <CompB b={b} />
    <CompC c={c} />
  </div>
);

const ConnectedBigComponent = connect(
  ({ a, b, c }) => ({ a, b, c })
);

Теперь каждый раз, когда изменяется a, b или c, BigComponent, включая CompA, CompB и CompC, будет повторно визуализироваться.

Вместо этого разделите компоненты и не бойтесь использовать connect:

const ConnectedA = connect(CompA, ({ a }) => ({ a }));
const ConnectedB = connect(CompB, ({ b }) => ({ b }));
const ConnectedC = connect(CompC, ({ c }) => ({ c }));

const BigComponent = () => (
  <div>
    <ConnectedA a={a} />
    <ConnectedB b={b} />
    <ConnectedC c={c} />
  </div>
);

В этом обновлении CompA будет повторно визуализироваться только при изменении a, CompB при изменении b и т. Д. Рассмотрим сценарий, в котором каждое значение a, b и c обновляется часто. Теперь для каждого обновления мы повторно выполняем рендеринг одного, а не всех компонентов. Это едва заметно с тремя компонентами, но что делать, если у вас их намного больше!

Преобразуйте свое состояние, чтобы оно было минимальным

Вот гипотетический (слегка надуманный) пример:

У вас большой список предметов, скажем, 300 или больше.

<List>
  {this.props.items.map(({ content, itemId }) => (
    <ListItem
      onClick={selectItem}
      content={content}
      itemId={itemId}
      key={itemId}
    />
  ))}
</List>

Когда мы щелкаем элемент списка, запускается действие с обновлением значения магазина - selectedItem. Каждый элемент списка подключается к Redux и получает selectedItem:

const ListItem = connect(
  ({ selectedItem }) => ({ selectedItem })
)(SimpleListItem);

Мы поступаем правильно, подключаем компонент только к тому состоянию, которое ему нужно. Однако когда selectedItem обновляется, все ListItem компоненты повторно обрабатываются, потому что объект, который мы возвращаем из selectedItem, изменился. Раньше было { selectedItem: 123 }, теперь { selectedItem: 120 }.

Теперь имейте в виду, что мы используем значение selectedItem, чтобы проверить, выбран ли текущий элемент. Итак, единственное, что действительно нужно знать нашему компоненту, - это выбран он или нет - по сути, Boolean. Логические значения хороши, поскольку есть только два возможных состояния: true или false. Итак, если мы вернем логическое значение вместо selectedItem, только два элемента, для которых логические изменения будут повторно отрисованы, и это все, что нам нужно. mapStateToProps фактически принимает компоненты props в качестве второго аргумента, мы можем использовать это, чтобы проверить, действительно ли это выбранный элемент. Вот как это будет выглядеть:

const ListItem = connect(
  ({ selectedItem }, { itemId }) => ({ isSelected: selectedItem === itemId })
)(SimpleListItem);

Теперь всякий раз, когда изменяется значение selectedItem, выполняется повторный рендеринг только двух компонентов - ListItem, который сейчас выбран, и тот, который не был выбран.

Держите ваши данные на одном уровне

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

{
  articles: [{
    comments: [{
      users: [{
      }]
    }]
  }],
  ...
}

Чтобы оптимизировать наши компоненты Article, Comment и User, нам теперь нужно подписать их все на articles, а затем глубоко погрузиться в эту структуру, чтобы вернуть только то состояние, которое им нужно. Имеет смысл вместо этого расположить вашу фигуру так:

{
  articles: [{
    ...
  }],
  comments: [{
    articleId: ..,
    userId: ...,
    ...
  }],
  users: [{
    ...
  }]
}

А затем выберите комментарии и информацию о пользователе с вашими функциями сопоставления. Подробнее об этом можно прочитать в документации Redux по нормализации формы состояния.

Бонус: библиотеки для выбора состояния Redux

Это совершенно необязательно и зависит от вас. Как правило, все приведенные выше советы должны увести вас достаточно далеко, чтобы писать быстрые приложения с React и Redux. Но есть две отличные библиотеки, которые упрощают выбор состояния:

Reselect - отличный инструмент для написания selectors вашего приложения Redux. Из повторно выбранных документов:

  • Селекторы могут вычислять производные данные, позволяя Redux сохранять минимально возможное состояние.
  • Селекторы эффективны. Селектор не пересчитывается, пока не изменится один из его аргументов.
  • Селекторы можно компоновать. Их можно использовать в качестве входных данных для других селекторов.

Для приложений со сложным интерфейсом, сложным состоянием и / или частыми обновлениями повторный выбор может очень сильно помочь сделать ваше приложение быстрее!

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

Первоначально опубликовано на http://reactrocket.com/post/react-redux-optimization/.