Продуктивність великого списку за допомогою React


86

Я в процесі впровадження списку, який можна фільтрувати, за допомогою React. Структура списку така, як показано на малюнку нижче.

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

ПРОМІСЛЯ

Ось опис того, як це має працювати:

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

  • Користувач вводить у Inputкомпонент більше 2 літер , масив фільтрується і для кожного запису у відфільтрованому масиві відображається 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?
Дібесджр,


Ах я розумію, спасибі. Отже, щоб відповісти на одне з ваших запитань, там сказано, що існує розбіжність в оптимізації між версіями. Одне, на що слід звернути увагу у великих списках, - це створення функцій у візуалізації. Коли ви потрапите в гігантські списки, він отримає хіт продуктивності. Я спробував би побачити, скільки часу потрібно для створення цього списку, використовуючи їхні інструменти для роботи з
лицем

2
Я думаю, вам слід переглянути, використовуючи Redux, тому що це саме те, що вам тут потрібно (або будь-який інший варіант реалізації). Вам слід остаточно поглянути на цю презентацію: Big List High Performance React & Redux
Pierre Criulanscy

2
Я сумніваюся, що користувач має якусь перевагу, прокручуючи 10000 результатів. То що, якщо ви лише рендеріть 100 найкращих результатів або оновите їх і оновлюєте на основі запиту.
Коен.

Відповіді:


18

Як і у багатьох інших відповідях на це питання, головна проблема полягає в тому, що рендерінг такої кількості елементів у DOM, виконуючи фільтрацію та обробку ключових подій, буде повільним.

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

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

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

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

Це далеко не production readyкод, але він ілюструє концепцію адекватно і може бути модифікований, щоб бути більш надійним, не соромтеся поглянути на код - я сподіваюся, щонайменше, це дає вам деякі ідеї ...;)

example-large-list-example

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


1
Мені справді погано, тому що мені довелося вибрати лише одну відповідь, всі вони, здається, доклали до цього зусиль, але я зараз перебуваю у відпустці, не маючи ПК, і не можу насправді перевірити їх з увагою, яку вони заслуговують. Я вибрав цей, тому що він досить короткий і чіткий, щоб зрозуміти його навіть під час читання з телефону. Кульгава причина, яку я знаю.
Dimitris Karagiannis

Що ви маєте на увазі під редагуванням хост-файлу 127.0.0.1 * http://localhost:3001?
stackjlei

@stackjlei Я думаю, він мав на увазі відображення 127.0.0.1 до localhost: 3001 в / etc / hosts
Маверік

16

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

Повільна частина реакції в даний момент - це коли вона порівнює різницю між віртуальним DOM і реальним DOM. Якщо у вас є тисячі компонентів, але ви оновлюєте лише пару, це не має значення, реакція все ще має величезну різницю між DOM.

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

Що я маю на увазі: рендеріть лише ті компоненти, які ви можете побачити в даний час, рендеріть більше, коли ви прокручуєте вниз, ви, швидше за все, користувач не прокрутите тисячі компонентів у будь-який спосіб .... Сподіваюся.

Чудова бібліотека для цього:

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

Тут є чудові інструкції:

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

Я боюся, що це не видаляє компоненти, які знаходяться вгорі сторінки, тому, якщо ви прокручуєте досить довго, ваші проблеми з продуктивністю почнуть знову виникати.

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


2
Оновлення: пакет, що міститься у цій відповіді, не підтримується. На npmjs.com/package/react-infinite-scroller
Ali Al Amine

11

Перш за все, різниця між розробницькою та виробничою версією React величезна, оскільки у виробництві є багато обхідних перевірок стану обґрунтованості (таких як перевірка типів опори).

Тоді, я думаю, вам слід переглянути думку про використання Redux, оскільки тут було б надзвичайно корисно для того, що вам потрібно (або будь-якої іншої реалізації потоку). Вам слід остаточно поглянути на цю презентацію: Big Performance High Performance React & Redux .

Але перед тим, як зануритися в redux, вам потрібно внести деякі корективи у ваш код React, розділивши компоненти на менші компоненти, оскільки shouldComponentUpdateце повністю обійде візуалізацію дітей, тому це величезний виграш .

Коли у вас більше детальних компонентів, ви можете обробляти стан за допомогою redux і реагувати-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')
);

Уроки, отримані за допомогою цього міні-додатку під час роботи з величезними наборами даних

  • Реагуючі компоненти найкраще працюють, коли їх не вистачає
  • Повторний вибір стає дуже корисним, щоб уникнути переобчислення та зберегти той самий об’єкт посилання (при використанні immutable.js) з однаковими аргументами.
  • Створіть connectкомпонент ed для компонента, який є найближчим до потрібних даних, щоб уникнути того, щоб компонент передавав лише реквізити, які вони не використовують
  • Використання функції тканини для створення mapDispatchToProps, коли вам потрібен лише початковий опис, ownPropsнеобхідний, щоб уникнути марного повторного візуалізації
  • Зреагуйте і відновіть остаточне поєднання!

2
Я не думаю, що додавання залежності до redux є необхідним для вирішення проблеми OP, подальші додаткові диспетчерські дії для фільтрації його набору результатів лише ускладнюють проблему, відправки не такі недорогі, як можна подумати, обробляючи цю конкретну ситуацію за допомогою локального компонента штат є найефективнішим підходом
deowk

4
  1. Реагування у версії розробки перевіряє наявність прототипів кожного компонента, щоб полегшити процес розробки, тоді як у виробництві він опущений.

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

  3. Іншою проблемою може бути сам величезний список. Ви можете створювати віртуальний макет і повторно використовувати створені елементи, просто замінюючи дані. В основному ви створюєте прокручуваний компонент контейнера з фіксованою висотою, всередині якого ви помістите контейнер списку. Висоту контейнера списку слід встановлювати вручну (itemHeight * numberOfItems) залежно від довжини видимого списку, щоб працювала смуга прокрутки. Потім створіть кілька компонентів елементів, щоб вони заповнювали висоту прокручуваних контейнерів і, можливо, додавали додатковий один або два імітаційні ефекти безперервного списку. зробіть їх абсолютною позицією, а при прокрутці просто перемістіть їх позицію, щоб вона імітувала безперервний список (я думаю, ви дізнаєтесь, як це реалізувати :)

  4. Ще одна річ - це писати в DOM - це також дорога операція, особливо якщо ви робите це неправильно. Ви можете використовувати полотно для відображення списків і створити плавний досвід прокрутки. Оформити компонент реакційного полотна. Я чув, що вони вже виконали певну роботу зі Списками.


Будь-яка інформація про React in development? і навіщо перевіряти наявність прототипів кожного компонента?
Люйль,

4

Ознайомтесь з React Virtualized Select, він розроблений для вирішення цієї проблеми та вражає своїми успіхами. З опису:

HOC, який використовує віртуалізовану реакцію та реакцію-вибір для відображення великих списків опцій у спадному меню

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


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>
  )
}

Скрипка, що містить повний приклад роботи: https://jsfiddle.net/koenpunt/hm1xnpqk/


3

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

Я не думаю, що ви, але не використовуйте індекси як ключі .

Щоб дізнатися справжню причину, чому версія для розробки та виробництва відрізняється, ви можете спробувати profilingсвій код.

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


2

Для тих, хто бореться з цією проблемою, я написав компонент, react-big-listякий обробляє списки до 1 мільйона записів.

На додаток до цього він має деякі фантастичні додаткові функції, такі як:

  • Сортування
  • Кешування
  • Спеціальна фільтрація
  • ...

Ми використовуємо його у виробництві в деяких додатках, і він чудово працює.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.