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

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

ПОМЕЩЕНИЕ

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

Состояние находится в компоненте самого высокого уровня,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, все работало гладко, без задержек.

НИЖНЯЯ ЛИНИЯ

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

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

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

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

Решение очень похоже на предложенное Аскаровым Бекнаромответниже

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

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

 Piyush.kapoor26 июн. 2016 г., 07:53
Используйте ключ в качестве индекса, а не файла
 Kokovin Vladislav26 июн. 2016 г., 17:25
@MitchKarajohn вы используете редукс?
 deowk04 июл. 2016 г., 21:52
Вам все еще нужно было бы прокручивать или вы хотите иметь возможность перемещаться по результатам только с помощью клавиш со стрелками?
 Dibesjr26 июн. 2016 г., 02:17
Что вы подразумеваете под разработкой / производственной версией реакции?
 Dimitris Karagiannis05 июл. 2016 г., 01:10
@Koen. На самом деле это правильное наблюдение, и это было бы умным решением.
 Dimitris Karagiannis26 июн. 2016 г., 17:31
@utro Нет, на данном этапе мне это не нужно
 Koen.05 июл. 2016 г., 01:06
Я сомневаюсь, что пользователь имеет какую-то выгоду от просмотра 10000 результатов. Так что, если вы только визуализируете результаты топ-100 или около того, и обновляете их на основе запроса.
 Dimitris Karagiannis26 июн. 2016 г., 02:25
Да, я думаю, я просто поражен, насколько велика разница между ними. При использовании версии dev рендеринг занимает около 300-500 мс каждый раз, когда изменяется выбранный результат. РЕДАКТИРОВАТЬ: спасибо, я проверю это
 Dibesjr26 июн. 2016 г., 02:23
Ах, я вижу, спасибо. Поэтому, отвечая на один из ваших вопросов, он говорит о несоответствии в оптимизации между версиями. В больших списках следует обратить внимание на создание функций в вашем рендере. Это приведет к снижению производительности, когда вы попадете в гигантские списки. Я хотел бы попытаться увидеть, сколько времени потребуется, чтобы создать этот список с помощью их инструментов Perffacebook.github.io/react/docs/perf.html
 Dimitris Karagiannis26 июн. 2016 г., 02:19
 Dimitris Karagiannis04 июл. 2016 г., 21:57
@deowk Мне тоже нужна прокрутка, но я решил прокрутку
 Pierre Criulanscy28 июн. 2016 г., 13:14
Я думаю, вам следует пересмотреть вопрос об использовании Redux, потому что это именно то, что вам нужно здесь (или любой другой поток реализации). Вы должны окончательно взглянуть на эту презентацию:High List High Performance React & Redux

Ответы на вопрос(8)

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

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

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

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

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

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

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

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

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) с теми же аргументами.Создайтеconnected компонент для компонента, который является самым близким из данных, которые им необходимы, чтобы компонент не передавал только реквизиты, которые они не используютИспользование функции ткани для создания mapDispatchToProps, когда вам нужна только начальная опора, указанная вownProps необходимо избегать бесполезного повторного рендерингаРеагируй и сокращай вместе!
 deowk05 июл. 2016 г., 10:54
Я не думаю, что добавление зависимости к redux необходимо для решения проблемы OP, дальнейшие более диспетчерские действия для фильтрации его результирующего набора только усугубят проблему, отправки не так дешевы, как вы могли бы подумать, обрабатывая эту конкретную ситуацию с локальным компонентом государство является наиболее эффективным подходом

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

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

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

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

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

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

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

Как я уже упоминал вмой комментарийЯ сомневаюсь, что пользователям нужны все эти 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/69z2wepo/47841/

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

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

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

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

 Liuuil06 мая 2018 г., 03:35
Любая информация оReact in development? а зачем проверяет прототипы каждого компонента?

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

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

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

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

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

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

С отличными практическими рекомендациями здесь:

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

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

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

 Ali Al Amine08 июл. 2018 г., 20:53
Обновление: пакет в этом ответе не поддерживается. Вилка настроена наnpmjs.com/package/react-infinite-scroller
Решение Вопроса

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

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

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

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

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

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

https://github.com/deowk/react-large-list-example

 Dimitris Karagiannis06 июл. 2016 г., 17:24
Мне очень жаль, что мне приходится выбирать только один ответ, кажется, что все они приложили усилия, но сейчас я нахожусь в отпуске без компьютера и не могу проверить их с вниманием, которого они заслуживают. Я выбрал этот, потому что он достаточно короткий и, чтобы понять, даже при чтении с телефона. Хромая причина, я знаю.
 Maverick08 февр. 2019 г., 14:38
@stackjlei Я думаю, он имел в виду отображение 127.0.0.1 влокальный: 3001 в / etc / hosts
 stackjlei29 сент. 2017 г., 02:26
Что вы подразумеваете под редактировать файл хоста127.0.0.1 * http://localhost:3001?

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

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

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

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

Ваш ответ на вопрос