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

Реагировать на производительность: рендеринг большого списка с помощью PureRenderMixin

Я принял пример TodoList, чтобы отразить мою проблему, но, очевидно, мой код в реальном мире более сложный.

У меня есть такой псевдокод.

var Todo = React.createClass({
  mixins: [PureRenderMixin], 
  ............ 
}

var TodosContainer = React.createClass({
  mixins: [PureRenderMixin],    

  renderTodo: function(todo) {
     return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
  },

  render: function() {
     var todos = this.props.todos.map(this.renderTodo)
     return (
          <ReactCSSTransitionGroup transitionName="transition-todo">
                 {todos}
          </ReactCSSTransitionGroup>,
     );
  }

});

Все мои данные неизменяемы, и PureRenderMixin используется надлежащим образом, и все работает нормально. Когда данные Todo изменяются, повторно отображается только родительский и отредактированный todo.

Проблема в том, что в какой-то момент мой список растет, когда пользователь прокручивается. И когда обновляется одно Todo, требуется все больше и больше времени для рендеринга родителя, вызывайте shouldComponentUpdate на всех todos, а затем визуализируйте одиночное todo.

Как вы можете видеть, компонент Todo имеет другой компонент, чем данные Todo. Это данные, которые необходимы для рендеринга всеми todos и являются общедоступными (например, мы могли бы представить там "displayMode" для todos). Наличие многих свойств делает shouldComponentUpdate немного медленнее.

Кроме того, использование ReactCSSTransitionGroup также немного замедляется, так как ReactCSSTransitionGroup должен отображать себя и ReactCSSTransitionGroupChild еще до вызова shouldComponentUpdate todos. React.addons.Perf показывает, что ReactCSSTransitionGroup > ReactCSSTransitionGroupChild рендеринг является временем, потраченным впустую для каждого элемента списка.

Итак, насколько я знаю, я использую PureRenderMixin, но с большим списком этого может быть недостаточно. Я по-прежнему получаю не очень плохие представления, но хотел бы знать, есть ли простые способы оптимизации моих рендерингов.

Любая идея?


Edit

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

Однако моя производительность рендера по-прежнему линейна для номера страницы (O (n)). Так что, если у меня есть тысячи страниц, это все равно та же проблема:) В моем случае это вряд ли произойдет, но меня все еще интересует лучшее решение.

Я уверен, что можно добиться производительности вывода O (log (n)), где n - количество элементов (или страниц), путем разделения большого списка на дерево (например, постоянная структура данных) и где каждый node имеет возможность короткого замыкания вычисления с помощью shouldComponentUpdate

Да, я думаю о том, что похоже на постоянные структуры данных, такие как Vector в Scala или Clojure:

http://hypirion.com/imgs/pvec/9-annotated.png

Однако меня беспокоит React, потому что, насколько я знаю, может возникнуть необходимость создавать промежуточные dom-узлы при рендеринге внутренних узлов дерева. Это может быть проблемой в соответствии с usecase (и может быть решена в будущих версиях React)

Также, поскольку мы используем Javascript, мне интересно, поддерживает ли Immutable-JS это и делает доступными "внутренние узлы". См.: https://github.com/facebook/immutable-js/issues/541

Изменить: полезная ссылка с моими экспериментами: Может ли приложение React-Redux действительно масштабироваться, а также, скажем, Backbone? Даже с повторным выбором. На мобильном телефоне

4b9b3361

Ответ 1

Вот реализация POC, которую я сделал с внутренней структурой ImmutableJS. Это не общедоступный API, поэтому он не готов к производству и в настоящее время не обрабатывает угловые случаи, но он работает.

var ImmutableListRenderer = React.createClass({
  render: function() {
    // Should not require to use wrapper <span> here but impossible for now
    return (<span>
        {this.props.list._root ? <GnRenderer gn={this.props.list._root}/> : undefined}
        {this.props.list._tail ? <GnRenderer gn={this.props.list._tail}/> : undefined}
</span>);
  }   
})

// "Gn" is the equivalent of the "internal node" of the persistent data structure schema of the question
var GnRenderer = React.createClass({
    shouldComponentUpdate: function(nextProps) {
      console.debug("should update?",(nextProps.gn !== this.props.gn));
      return (nextProps.gn !== this.props.gn);
    },
    propTypes: {
        gn: React.PropTypes.object.isRequired,
    },
    render: function() {
        // Should not require to use wrapper <span> here but impossible for now
        return (
            <span>
                {this.props.gn.array.map(function(gnItem,index) { 
                    // TODO should check for Gn instead, because list items can be objects too...
                    var isGn = typeof gnItem === "object"
                    if ( isGn ) {
                        return <GnRenderer gn={gnItem}/>
                    } else {
                        // TODO should be able to customize the item rendering from outside
                        return <span>{" -> " + gnItem}</span>
                    }
                }.bind(this))}
            </span>
        );
    }
})

Код клиента выглядит как

React.render(
    <ImmutableListRenderer list={ImmutableList}/>, 
    document.getElementById('container')
);

Вот JsFiddle, который регистрирует количество вызовов shouldComponentUpdate после обновления одного элемента списка (размер N): это не требует для вызова N раз shouldComponentUpdate

Дальнейшие подробности реализации разделяются в этой проблеме ImmutableJs github

Ответ 2

В нашем продукте у нас также были проблемы, связанные с количеством отображаемого кода, и мы начали использовать наблюдаемые (см. blog). Это может частично решить вашу проблему, так как изменение todo больше не потребует родительского компонента, который содержит список, который нужно перерисовать (но добавление все еще делает).

Это может также помочь вам в повторном рендеринге списка быстрее, поскольку ваши компоненты todoItem могут просто вернуть false, когда реквизит изменится на shouldComponentUpdate.

Для улучшения производительности при рендеринге обзора, я думаю, что ваша идея tree/paging действительно хороша. С наблюдаемыми массивами каждая страница может начать слушать массивные сращивания (используя ES7 polyfill или mobservable) в определенном диапазоне. Это приведет к некоторому администрированию, поскольку эти диапазоны могут меняться со временем, но должны привести вас к O (log (n))

Итак, вы получаете что-то вроде:

var TodosContainer = React.createClass({
  componentDidMount() {
     this.props.todos.observe(function(change) {
         if (change.type === 'splice' && change.index >= this.props.startRange && change.index < this.props.endRange)
             this.forceUpdate();
     });
  },    

  renderTodo: function(todo) {
     return <Todo key={todo.id} todoData={todo} x={this.props.x} y={this.props.y} .../>;
  },

  render: function() {
     var todos = this.props.todos.slice(this.props.startRange, this.props.endRange).map(this.renderTodo)
     return (
          <ReactCSSTransitionGroup transitionName="transition-todo">
                 {todos}
          </ReactCSSTransitionGroup>,
     );
  }

});

Центральная проблема с большими списками и реагированием кажется, что вы не можете просто переместить новые узлы DOM в dom. В противном случае вам не нужны "страницы" вообще, чтобы разделить данные на более мелкие куски, и вы можете просто соединить один новый элемент Todo в dom, как это сделано с JQuery в этом jsFiddle. Вы все еще можете это сделать, если будете реагировать, если вы используете ref для каждого пункта todo, но это будет работать вокруг системы, я думаю, поскольку это может нарушить систему согласования?