Подтвердить что ты не робот

Как оптимизировать небольшие обновления для реквизитов вложенного компонента в React + Redux?

Пример кода: https://github.com/d6u/example-redux-update-nested-props/blob/master/one-connect/index.js

Открыть демо-версию: http://d6u.github.io/example-redux-update-nested-props/one-connect.html

Как оптимизировать небольшие обновления для реквизитов вложенного компонента?

У меня выше компонентов, Repo и RepoList. Я хочу обновить тег первого репо (Строка 14). Поэтому я отправил действие UPDATE_TAG. Прежде чем я выполнил shouldComponentUpdate, отправка займет около 200 мс, что ожидается, так как мы тратим много времени на изменение <Repo/>, которые не изменились.

После добавления shouldComponentUpdate отправка занимает около 30 мс. После сборки сборки React.js обновления будут стоить около 17 мс. Это намного лучше, но просмотр временной шкалы в консоли Chrome dev по-прежнему показывает jank-фрейм (длиннее 16,6 мс).

введите описание изображения здесь

Представьте себе, если у нас много таких обновлений, или <Repo/> более сложный, чем текущий, мы не сможем поддерживать 60 кадров в секунду.

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

Я получил решение, заменив каждый tags на наблюдаемый внутренний редуктор. Что-то вроде

// inside reducer when handling UPDATE_TAG action
// repos[0].tags of state is already replaced with a Rx.BehaviorSubject
get('repos[0].tags', state).onNext([{
  id: 213,
  text: 'Node.js'
}]);

Затем я подписываюсь на их значения внутри компонента Repo, используя https://github.com/jayphelps/react-observable-subscribe. Это отлично поработало. Каждая отправка только стоит 5 мс даже с разработкой React.js. Но я чувствую, что это анти-шаблон в Redux.

Обновление 1

Я следил за рекомендацией в ответе Дэна Абрамова и нормализовал мое состояние и обновленные компоненты подключения

Новая форма состояния:

{
    repoIds: ['1', '2', '3', ...],
    reposById: {
        '1': {...},
        '2': {...}
    }
}

Я добавил console.time вокруг ReactDOM.render во время первоначальный рендеринг.

Однако производительность хуже, чем раньше (как первоначальная рендеринг, так и обновление). (Источник: https://github.com/d6u/example-redux-update-nested-props/blob/master/repo-connect/index.js, демо-версия: http://d6u.github.io/example-redux-update-nested-props/repo-connect.html)

// With dev build
INITIAL: 520.208ms
DISPATCH: 40.782ms

// With prod build
INITIAL: 138.872ms
DISPATCH: 23.054ms

введите описание изображения здесь

Я думаю, что соединение на каждом <Repo/> имеет много накладных расходов.

Обновление 2

Основываясь на обновленном ответе Дэна, мы должны вернуть аргументы connect mapStateToProps, вместо этого возвращая функцию. Вы можете проверить ответ Дэна. Я также обновил демо.

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

// in prod build (not average, very small sample)

// one connect at root
INITIAL: 83.789ms
DISPATCH: 17.332ms

// connect at every <Repo/>
INITIAL: 126.557ms
DISPATCH: 22.573ms

// connect at every <Repo/> with memorization
INITIAL: 125.115ms
DISPATCH: 9.784ms

// observables + side effect in reducers (don't use!)
INITIAL: 163.923ms
DISPATCH: 4.383ms

Обновление 3

Просто добавлен реактивно-виртуализированный пример на основе "connect at every with memorization"

INITIAL: 31.878ms
DISPATCH: 4.549ms
4b9b3361

Ответ 1

Я не уверен, откуда приходит const App = connect((state) => state)(RepoList).
соответствующий пример в документах React Redux имеет уведомление:

Не делай этого! Он убивает любую оптимизацию производительности, потому что TodoApp будет повторно получать после каждого действия. Лучше иметь более подробное соединение() по нескольким компонентам в иерархии вашего представления, каждое из которых послушайте соответствующий срез состояния.

Мы не предлагаем использовать этот шаблон. Скорее, каждый подключает <Repo> специально, чтобы он считывал свои собственные данные в своем mapStateToProps. Пример tree-view показывает, как это сделать.

Если вы сделаете форму состояния более нормированной (сейчас ее все вложенное), вы можете отделить repoIds от reposById, а затем иметь только RepoList re-render, если repoIds изменится. Таким образом, изменения в отдельных репозиториях не будут влиять на сам список, и только соответствующий Repo получит повторную визуализацию. Этот запрос на перенос может дать вам представление о том, как это может работать. Пример real-world показывает, как вы можете писать редукторы, которые имеют дело с нормализованными данными.

Обратите внимание, что для того, чтобы действительно извлечь выгоду из производительности, предлагаемой путем нормализации дерева, вам нужно сделать то же самое, что этот запрос на перенос делает и передает mapStateToProps() factory до connect():

const makeMapStateToProps = (initialState, initialOwnProps) => {
  const { id } = initialOwnProps
  const mapStateToProps = (state) => {
    const { todos } = state
    const todo = todos.byId[id]
    return {
      todo
    }
  }
  return mapStateToProps
}

export default connect(
  makeMapStateToProps
)(TodoItem)

Причина, по которой это важно, состоит в том, что мы знаем, что идентификаторы никогда не меняются. Использование ownProps имеет ограничение производительности: внутренние реквизиты должны быть пересчитаны в любое время, когда внешние реквизиты меняются. Однако использование initialOwnProps не несет этого штрафа, потому что оно используется только один раз.

Быстрая версия вашего примера будет выглядеть так:

import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import {Provider, connect} from 'react-redux';
import set from 'lodash/fp/set';
import pipe from 'lodash/fp/pipe';
import groupBy from 'lodash/fp/groupBy';
import mapValues from 'lodash/fp/mapValues';

const UPDATE_TAG = 'UPDATE_TAG';

const reposById = pipe(
  groupBy('id'),
  mapValues(repos => repos[0])
)(require('json!../repos.json'));

const repoIds = Object.keys(reposById);

const store = createStore((state = {repoIds, reposById}, action) => {
  switch (action.type) {
  case UPDATE_TAG:
    return set('reposById.1.tags[0]', {id: 213, text: 'Node.js'}, state);
  default:
    return state;
  }
});

const Repo  = ({repo}) => {
  const [authorName, repoName] = repo.full_name.split('/');
  return (
    <li className="repo-item">
      <div className="repo-full-name">
        <span className="repo-name">{repoName}</span>
        <span className="repo-author-name"> / {authorName}</span>
      </div>
      <ol className="repo-tags">
        {repo.tags.map((tag) => <li className="repo-tag-item" key={tag.id}>{tag.text}</li>)}
      </ol>
      <div className="repo-desc">{repo.description}</div>
    </li>
  );
}

const ConnectedRepo = connect(
  (initialState, initialOwnProps) => (state) => ({
    repo: state.reposById[initialOwnProps.repoId]
  })
)(Repo);

const RepoList = ({repoIds}) => {
  return <ol className="repos">{repoIds.map((id) => <ConnectedRepo repoId={id} key={id}/>)}</ol>;
};

const App = connect(
  (state) => ({repoIds: state.repoIds})
)(RepoList);

console.time('INITIAL');
ReactDOM.render(
  <Provider store={store}>
    <App/>
  </Provider>,
  document.getElementById('app')
);
console.timeEnd('INITIAL');

setTimeout(() => {
  console.time('DISPATCH');
  store.dispatch({
    type: UPDATE_TAG
  });
  console.timeEnd('DISPATCH');
}, 1000);

Обратите внимание, что я изменил connect() в ConnectedRepo, чтобы использовать factory с initialOwnProps, а не ownProps. Это позволяет React Redux пропустить всю повторную оценку prop.

Я также удалил ненужный shouldComponentUpdate() на <Repo>, потому что React Redux позаботится о его реализации в connect().

Этот подход превосходит оба предыдущих подхода в моем тестировании:

one-connect.js: 43.272ms
repo-connect.js before changes: 61.781ms
repo-connect.js after changes: 19.954ms

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


Я получил решение, заменив все теги наблюдаемым внутренним редуктором.

Если у него есть побочные эффекты, это не редуктор Redux. Это может сработать, но я предлагаю поставить такой код вне Redux, чтобы избежать путаницы. Редукторы Redux должны быть чистыми функциями, и они не могут называть onNext для объектов.