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

Большая производительность списка с помощью React

Я нахожусь в процессе реализации фильтруемого списка с React. Структура списка показана на рисунке ниже.

enter image description here

ПОМЕЩЕНИЕ

Вот описание того, как это должно работать:

  • Состояние находится в компоненте самого высокого уровня, компоненте Search.
  • Состояние описывается следующим образом:
{
    visible : boolean,
    files : array,
    filtered : array,
    query : string,
    currentlySelectedIndex : integer
}
  • files - потенциально очень большой массив, содержащий пути к файлам (10000 записей - вероятное число).
  • filtered - фильтруемый массив после того, как пользователь вводит не менее 2 символов. Я знаю, что это производные данные, и поэтому можно привести аргумент о том, что они должны храниться в состоянии, но это необходимо для
  • currentlySelectedIndex, который является индексом выбранного в данный момент элемента из отфильтрованного списка.

  • Пользователь вводит более 2 букв в компонент Input, массив фильтруется, и для каждой записи в фильтрованном массиве отображается компонент Result

  • Каждый компонент Result отображает полный путь, который частично соответствует запросу, и часть пути с частичным совпадением выделяется. Например, DOM компонента Result, если бы пользователь ввел 'le', был бы примерно таким:

    <li>this/is/a/fi<strong>le</strong>/path</li>

  • Если пользователь нажимает клавиши вверх или вниз, когда компонент Input находится в фокусе, изменения currentlySelectedIndex основываются на массиве filtered. Это приводит к тому, что компонент Result, соответствующий индексу, помечается как выбранный, вызывая повторную визуализацию

ПРОБЛЕМА

Сначала я протестировал это с достаточно маленьким массивом files, используя версию React для разработки, и все работало нормально.

Проблема возникла, когда мне пришлось иметь дело с массивом files размером до 10000 записей. Если ввести 2 буквы во Ввод, получится большой список, и когда я нажимаю клавиши "вверх" и "вниз", чтобы перемещаться по нему, это будет очень медленно.

Сначала у меня не было определенного компонента для элементов Result, и я просто создавал список на лету для каждого рендера компонента Search, как таковой:

results  = this.state.filtered.map(function(file, index) {
    var start, end, matchIndex, match = this.state.query;

     matchIndex = file.indexOf(match);
     start = file.slice(0, matchIndex);
     end = file.slice(matchIndex + match.length);

     return (
         <li onClick={this.handleListClick}
             data-path={file}
             className={(index === this.state.currentlySelected) ? "valid selected" : "valid"}
             key={file} >
             {start}
             <span className="marked">{match}</span>
             {end}
         </li>
     );
}.bind(this));

Как вы можете сказать, каждый раз, когда currentlySelectedIndex изменяется, это вызывает повторный рендеринг, и список будет создаваться каждый раз. Я подумал, что, поскольку я установил значение key для каждого элемента li, React будет избегать повторного рендеринга каждого другого элемента li, в котором не было изменения className, но, очевидно, это было не так.

В итоге я определил класс для элементов Result, где он явно проверяет, должен ли каждый элемент Result повторно визуализироваться на основе того, был ли он ранее выбран, и на основе текущего пользовательского ввода:

var ResultItem = React.createClass({
    shouldComponentUpdate : function(nextProps) {
        if (nextProps.match !== this.props.match) {
            return true;
        } else {
            return (nextProps.selected !== this.props.selected);
        }
    },
    render : function() {
        return (
            <li onClick={this.props.handleListClick}
                data-path={this.props.file}
                className={
                    (this.props.selected) ? "valid selected" : "valid"
                }
                key={this.props.file} >
                {this.props.children}
            </li>
        );
    }
});

И теперь список создан так:

results = this.state.filtered.map(function(file, index) {
    var start, end, matchIndex, match = this.state.query, selected;

    matchIndex = file.indexOf(match);
    start = file.slice(0, matchIndex);
    end = file.slice(matchIndex + match.length);
    selected = (index === this.state.currentlySelected) ? true : false

    return (
        <ResultItem handleClick={this.handleListClick}
            data-path={file}
            selected={selected}
            key={file}
            match={match} >
            {start}
            <span className="marked">{match}</span>
            {end}
        </ResultItem>
    );
}.bind(this));
}

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

BottomLine

Является ли такое заметное несоответствие между разработкой и производственной версией React нормальным?

Я понимаю/делаю что-то не так, когда думаю о том, как React управляет списком?

ОБНОВЛЕНИЕ 14-11-2016

Я нашел эту презентацию Майкла Джексона, где он решает проблему, очень похожую на эту: https://youtu.be/7S8v8jfLb1Q?t=26m2s

Решение очень похоже на предложенное AskarovBeknar answer, ниже

ОБНОВЛЕНИЕ 14-4-2018

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

4b9b3361

Ответ 1

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

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

Если ваш пользовательский интерфейс не разработан с учетом эффективности, пострадают даже такие инструменты, как React, предназначенные для повышения производительности.

Фильтрация набора результатов - отличное начало, как упомянул @Koen

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

Это ни в коем случае не код production ready, но он адекватно иллюстрирует концепцию и может быть изменен, чтобы быть более надежным, не стесняйтесь взглянуть на код - я надеюсь, по крайней мере, он дает вам некоторые идеи...; )

реагируют-большой-список-пример

enter image description here

Ответ 2

Мой опыт работы с очень похожей проблемой заключается в том, что реакция действительно страдает, если в DOM одновременно находится более 100-200 компонентов. Даже если вы будете очень осторожны (устанавливая все свои ключи и/или реализуя метод shouldComponentUpdate) только для того, чтобы изменить один или два компонента при повторном рендеринге, вы все равно окажетесь в мире боли.

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

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

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

Отличная библиотека для этого:

https://www.npmjs.com/package/react-infinite-scroll

С отличным практическим руководством:

http://www.reactexamples.com/react-infinite-scroll/

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

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

Ответ 3

Прежде всего, разница между версией разработки и производственной версией React огромна, потому что в производстве существует много обходных проверок работоспособности (таких как проверка типов пропов).

Затем, я думаю, вам следует пересмотреть вопрос об использовании Redux, потому что это было бы чрезвычайно полезно для того, что вам нужно (или для любой другой реализации). Вы должны обязательно взглянуть на эту презентацию: Big List High Performance React & Redux.

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

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

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

Вот как это выглядит:

enter image description here

В основном это 4 основных компонента (здесь только одна строка, но для примера):

enter image description here

Вот полный код (работает CodePen: Огромный список с React & Redux) с использованием redux, Reaction-redux, immutable, повторно выберите и перекомпоновайте:

const initialState = Immutable.fromJS({ /* See codepen, this is a HUGE list */ })

const types = {
    CONCERTS_DEDUP_NAME_CHANGED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_NAME_CHANGED',
    CONCERTS_DEDUP_CONCERT_TOGGLED: 'diggger/concertsDeduplication/CONCERTS_DEDUP_CONCERT_TOGGLED',
};

const changeName = (pk, name) => ({
    type: types.CONCERTS_DEDUP_NAME_CHANGED,
    pk,
    name
});

const toggleConcert = (pk, toggled) => ({
    type: types.CONCERTS_DEDUP_CONCERT_TOGGLED,
    pk,
    toggled
});


const reducer = (state = initialState, action = {}) => {
    switch (action.type) {
        case types.CONCERTS_DEDUP_NAME_CHANGED:
            return state
                .updateIn(['names', String(action.pk)], () => action.name)
                .set('_state', 'not_saved');
        case types.CONCERTS_DEDUP_CONCERT_TOGGLED:
            return state
                .updateIn(['concerts', String(action.pk)], () => action.toggled)
                .set('_state', 'not_saved');
        default:
            return state;
    }
};

/* configureStore */
const store = Redux.createStore(
    reducer,
    initialState
);

/* SELECTORS */

const getDuplicatesGroups = (state) => state.get('duplicatesGroups');

const getDuplicateGroup = (state, name) => state.getIn(['duplicatesGroups', name]);

const getConcerts = (state) => state.get('concerts');

const getNames = (state) => state.get('names');

const getConcertName = (state, pk) => getNames(state).get(String(pk));

const isConcertOriginal = (state, pk) => getConcerts(state).get(String(pk));

const getGroupNames = reselect.createSelector(
    getDuplicatesGroups,
    (duplicates) => duplicates.flip().toList()
);

const makeGetConcertName = () => reselect.createSelector(
    getConcertName,
    (name) => name
);

const makeIsConcertOriginal = () => reselect.createSelector(
    isConcertOriginal,
    (original) => original
);

const makeGetDuplicateGroup = () => reselect.createSelector(
    getDuplicateGroup,
    (duplicates) => duplicates
);



/* COMPONENTS */

const DuplicatessTableRow = Recompose.onlyUpdateForKeys(['name'])(({ name }) => {
    return (
        <tr>
            <td>{name}</td>
            <DuplicatesRowColumn name={name}/>
        </tr>
    )
});

const PureToggle = Recompose.onlyUpdateForKeys(['toggled'])(({ toggled, ...otherProps }) => (
    <input type="checkbox" defaultChecked={toggled} {...otherProps}/>
));


/* CONTAINERS */

let DuplicatesTable = ({ groups }) => {

    return (
        <div>
            <table className="pure-table pure-table-bordered">
                <thead>
                    <tr>
                        <th>{'Concert'}</th>
                        <th>{'Duplicates'}</th>
                    </tr>
                </thead>
                <tbody>
                    {groups.map(name => (
                        <DuplicatesTableRow key={name} name={name} />
                    ))}
                </tbody>
            </table>
        </div>
    )

};

DuplicatesTable.propTypes = {
    groups: React.PropTypes.instanceOf(Immutable.List),
};

DuplicatesTable = ReactRedux.connect(
    (state) => ({
        groups: getGroupNames(state),
    })
)(DuplicatesTable);


let DuplicatesRowColumn = ({ duplicates }) => (
    <td>
        <ul>
            {duplicates.map(d => (
                <DuplicateItem
                    key={d}
                    pk={d}/>
            ))}
        </ul>
    </td>
);

DuplicatessRowColumn.propTypes = {
    duplicates: React.PropTypes.arrayOf(
        React.PropTypes.string
    )
};

const makeMapStateToProps1 = (_, { name }) => {
    const getDuplicateGroup = makeGetDuplicateGroup();
    return (state) => ({
        duplicates: getDuplicateGroup(state, name)
    });
};

DuplicatesRowColumn = ReactRedux.connect(makeMapStateToProps1)(DuplicatesRowColumn);


let DuplicateItem = ({ pk, name, toggled, onToggle, onNameChange }) => {
    return (
        <li>
            <table>
                <tbody>
                    <tr>
                        <td>{ toggled ? <input type="text" value={name} onChange={(e) => onNameChange(pk, e.target.value)}/> : name }</td>
                        <td>
                            <PureToggle toggled={toggled} onChange={(e) => onToggle(pk, e.target.checked)}/>
                        </td>
                    </tr>
                </tbody>
            </table>
        </li>
    )
}

const makeMapStateToProps2 = (_, { pk }) => {
    const getConcertName = makeGetConcertName();
    const isConcertOriginal = makeIsConcertOriginal();

    return (state) => ({
        name: getConcertName(state, pk),
        toggled: isConcertOriginal(state, pk)
    });
};

DuplicateItem = ReactRedux.connect(
    makeMapStateToProps2,
    (dispatch) => ({
        onNameChange(pk, name) {
            dispatch(changeName(pk, name));
        },
        onToggle(pk, toggled) {
            dispatch(toggleConcert(pk, toggled));
        }
    })
)(DuplicateItem);


const App = () => (
    <div style={{ maxWidth: '1200px', margin: 'auto' }}>
        <DuplicatesTable />
    </div>
)

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

Уроки, извлеченные из этого мини-приложения при работе с огромным набором данных

  • Компоненты React работают лучше всего, когда они маленькие.
  • Повторный выбор становится очень полезным, чтобы избежать повторного вычисления и сохранить тот же ссылочный объект (при использовании immutable.js) с теми же аргументами.
  • Создайте компонент connect ed для компонента, который является ближайшим к ним необходимым, чтобы компонент не передавал только реквизиты, которые они не используют
  • Использование функции ткани для создания mapDispatchToProps, когда вам нужна только начальная опора, указанная в ownProps, необходимо, чтобы избежать бесполезного повторного рендеринга
  • Реагировать & окончательно слиться!

Ответ 4

  • Реагировать в версиях разработки проверяет, чтобы proptypes каждого компонента упрощал процесс разработки, в то время как в производстве он опускается.

  • Фильтрация списка строк - очень дорогостоящая операция для каждой клавиатуры. это может вызвать проблемы с производительностью из-за однопоточного характера JavaScript. Решение может заключаться в использовании метода debounce для задержки выполнения функции фильтра до истечения задержки.

  • Другой проблемой может быть огромный список. Вы можете создать виртуальный макет и повторно использовать созданные элементы, просто заменяя данные. В основном вы создаете прокручиваемый контейнерный компонент с фиксированной высотой, внутри которого вы разместите контейнер списка. Высота контейнера списка должна быть установлена ​​вручную (itemHeight * numberOfItems) в зависимости от длины видимого списка, чтобы иметь панель прокрутки. Затем создайте несколько компонентов компонента, чтобы они заполнили высоту прокручиваемых контейнеров и, возможно, добавили дополнительный один или два эффекта имитирующего непрерывный список. сделайте их абсолютное положение, а свиток просто переместит их положение так, что он будет имитировать непрерывный список (я думаю, вы узнаете, как его реализовать:)

  • Еще одна вещь - писать в DOM - тоже дорогостоящая операция, особенно если вы делаете это неправильно. Вы можете использовать холст для отображения списков и создания плавного перехода на прокрутку. Оформить контрольные компоненты. Я слышал, что они уже проделали определенную работу над списками.

Ответ 5

Как я уже упоминал в моем комментарии, я сомневаюсь, что пользователям нужны все эти 10000 результатов в браузере сразу.

Что делать, если вы просматриваете результаты и всегда показывает список из 10 результатов.

Я создал пример, используя эту технику, без использования какой-либо другой библиотеки, такой как Redux. В настоящее время только с клавиатурной навигацией, но может быть легко расширен и для работы с прокруткой.

Пример существует из 3 компонентов: приложения-контейнера, компонента поиска и компонента списка. Почти вся логика была перенесена в компонент контейнера.

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

nextResult: function() {
  var selected = this.state.selected + 1
  var start = this.state.start
  if(selected >= start + this.props.limit) {
    ++start
  }
  if(selected + start < this.state.results.length) {
    this.setState({selected: selected, start: start})
  }
},

prevResult: function() {
  var selected = this.state.selected - 1
  var start = this.state.start
  if(selected < start) {
    --start
  }
  if(selected + start >= 0) {
    this.setState({selected: selected, start: start})
  }
},

При простой передаче всех файлов через фильтр:

updateResults: function() {
  var results = this.props.files.filter(function(file){
    return file.file.indexOf(this.state.query) > -1
  }, this)

  this.setState({
    results: results
  });
},

И нарезка результатов на основе start и limit в методе render:

render: function() {
  var files = this.state.results.slice(this.state.start, this.state.start + this.props.limit)
  return (
    <div>
      <Search onSearch={this.onSearch} onKeyDown={this.onKeyDown} />
      <List files={files} selected={this.state.selected - this.state.start} />
    </div>
  )
}

Скрипка, содержащая полный рабочий пример: https://jsfiddle.net/koenpunt/hm1xnpqk/

Ответ 6

Ознакомьтесь с React Virtualized Select, он разработан для решения этой проблемы и показывает впечатляющие результаты. Из описания:

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

https://github.com/bvaughn/react-virtualized-select

Ответ 7

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

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

Чтобы узнать реальную причину разницы в версиях разработки и производства, вы можете попробовать profiling ваш код.

Загрузите свою страницу, начните запись, выполните изменения, прекратите запись, а затем проверьте тайминги. См. здесь для инструкций по профилированию производительности в Chrome.

Ответ 8

Для тех, кто борется с этой проблемой, я написал компонент react-big-list, который обрабатывает списки до 1 миллиона записей.

Вдобавок к этому он поставляется с некоторыми необычными дополнительными функциями, такими как:

  • Сортировка
  • Кэширование
  • Пользовательская фильтрация
  • ...

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