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

86

Я работаю над реализацией фильтруемого списка с помощью React. Структура списка показана на изображении ниже.

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

ПОМЕЩЕНИЕ

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

  • Состояние находится в компоненте самого высокого уровня, Searchкомпоненте.
  • Состояние описывается следующим образом:
{
    видимый: логический,
    файлы: массив,
    фильтруется: массив,
    Строка запроса,
    currentSelectedIndex: целое число
}
  • files потенциально очень большой массив, содержащий пути к файлам (10 000 записей - вероятное число).
  • 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

Решение очень похож на тот , предложенный AskarovBeknar в ответ ниже

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

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

Димитрис Карагианнис
источник
Что вы имеете в виду под разработкой / производственной версией React?
Dibesjr
@Dibesjr facebook.github.io/react/…
Димитрис Карагианнис
А, понятно, спасибо. Итак, чтобы ответить на один из ваших вопросов, он говорит, что есть расхождения в оптимизации между версиями. В больших списках нужно остерегаться создания функций в рендере. Когда вы попадете в гигантские списки, у него будет удар по производительности. Я бы попробовал посмотреть, сколько времени потребуется, чтобы сгенерировать этот список, используя их инструменты perf facebook.github.io/react/docs/perf.html
Dibesjr
2
Я думаю, вам следует пересмотреть использование Redux, потому что это именно то, что вам здесь нужно (или любая реализация потока). Вам следует окончательно взглянуть на эту презентацию: Big List High Performance React & Redux
Пьер Криуланси
2
Я сомневаюсь, что у пользователя есть какая-то выгода от прокрутки 10000 результатов. Так что, если вы отображаете только 100 лучших результатов или около того и обновляете их на основе запроса.
Коэн.

Ответы:

18

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

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

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

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

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

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

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

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

Deowk
источник
1
Мне очень жаль, что приходится выбирать только один ответ, кажется, все они приложили усилия, но сейчас я нахожусь в отпуске без компьютера и не могу проверить их с тем вниманием, которого они заслуживают. Я выбрал этот, потому что он достаточно короткий и по существу, чтобы его можно было понять даже при чтении с телефона. Хромая причина, которую я знаю.
Димитрис Карагианнис
Что вы имеете в виду под редактированием файла хоста 127.0.0.1 * http://localhost:3001?
stackjlei
@stackjlei Я думаю, он имел в виду сопоставление 127.0.0.1 с localhost: 3001 в / etc / hosts
Maverick
16

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

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

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

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

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

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

Здесь есть отличные инструкции:

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

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

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

Резонанс
источник
2
Обновление: пакет, содержащийся в этом ответе, не поддерживается. Форк
Али Аль Амин
11

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

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

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

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

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

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

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

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

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

Вот полный код (рабочий CodePen: огромный список с React и Redux ) с использованием redux , response-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 для компонента, который является ближайшим к данным, которые им нужны, чтобы компонент не передавал только свойства, которые они не используют
  • Использование функции Fabric для создания mapDispatchToProps, когда вам нужно только начальное свойство, указанное в ownProps, необходимо, чтобы избежать бесполезного повторного рендеринга
  • React & redux окончательно скачет вместе!
Пьер Криуланси
источник
2
Я не думаю, что добавление зависимости к redux необходимо для решения проблемы OP, дополнительные диспетчерские действия для фильтрации его набора результатов только усугубят проблему, диспетчеризации не так дешевы, как вы могли подумать, обработка этой конкретной ситуации с локальным компонентом состояние - самый эффективный подход
deowk 05
4
  1. React в разрабатываемой версии проверяет типы свойств каждого компонента, чтобы упростить процесс разработки, в то время как в производственной версии он не используется.

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

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

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

АскаровБекнар
источник
Есть информация о React in development? а зачем проверять прототипы каждого компонента?
Люуил
4

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

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

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

Madbreaks
источник
4

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

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

Коэн.
источник
3

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

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

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

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

RationalDev нравится GoFundMonica
источник
2

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

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

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

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

Meemaw
источник