Duża lista wydajności dzięki React

86

Jestem w trakcie wdrażania listy z możliwością filtrowania w React. Struktura listy jest taka, jak pokazano na poniższej ilustracji.

wprowadź opis obrazu tutaj

PRZESŁANKA

Oto opis tego, jak to powinno działać:

  • Stan znajduje się w komponencie najwyższego poziomu, Searchkomponencie.
  • Stan jest opisany następująco:
{
    widoczne: boolean,
    pliki: tablica,
    filtrowane: tablica,
    zapytanie: ciąg,
    currentSelectedIndex: integer
}
  • files to potencjalnie bardzo duża tablica zawierająca ścieżki do plików (10 000 wpisów to wiarygodna liczba).
  • filteredto przefiltrowana tablica po wpisaniu przez użytkownika co najmniej 2 znaków. Wiem, że to dane pochodne i jako taki można by argumentować o przechowywaniu ich w stanie, ale jest to potrzebne
  • currentlySelectedIndex który jest indeksem aktualnie wybranego elementu z przefiltrowanej listy.

  • Użytkownik wpisuje więcej niż 2 litery do Inputkomponentu, tablica jest filtrowana i dla każdego wpisu w filtrowanej tablicy Resultjest renderowany komponent

  • Każdy Resultskładnik wyświetla pełną ścieżkę, która częściowo pasuje do zapytania, a część ścieżki częściowego dopasowania jest podświetlona. Na przykład DOM komponentu wynikowego, gdyby użytkownik wpisał „le”, wyglądałby tak:

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

  • Jeśli użytkownik naciśnie klawisze w górę lub w dół, gdy Inputskładnik jest aktywny, currentlySelectedIndexzmiany oparte na filteredtablicy. Powoduje to Result, że komponent zgodny z indeksem zostanie oznaczony jako wybrany, co spowoduje ponowne renderowanie

PROBLEM

Początkowo testowałem to z wystarczająco małą tablicą files, używając rozwojowej wersji Reacta i wszystko działało dobrze.

Problem pojawił się, gdy miałem do czynienia z filestablicą liczącą aż 10000 wpisów. Wpisanie 2 liter na wejściu wygenerowałoby dużą listę, a kiedy naciskałem klawisze w górę iw dół, aby się po niej poruszać, byłoby bardzo opóźnione.

Na początku nie miałem zdefiniowanego komponentu dla Resultelementów i robiłem po prostu listę w locie, przy każdym renderowaniu Searchkomponentu, jako takiej:

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

Jak widać, za każdym razem, gdy currentlySelectedIndexzmiana powodowała ponowne renderowanie, a lista byłaby odtwarzana za każdym razem. Pomyślałem, że skoro ustawiłem keywartość dla każdego lielementu, React uniknie ponownego renderowania każdego innego lielementu, który nie miał swojej classNamezmiany, ale najwyraźniej tak nie było.

Skończyło się na tym Result, że zdefiniowałem klasę dla elementów, w której wyraźnie sprawdza, czy każdy Resultelement powinien być ponownie renderowany na podstawie tego, czy został wcześniej wybrany i na podstawie aktualnych danych wejściowych użytkownika:

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

A lista jest teraz tworzona jako taka:

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

To sprawiło, że wydajność jest nieco lepsza, ale nadal nie jest wystarczająco dobra. Rzecz w tym, że testowałem na produkcyjnej wersji Reacta, wszystko działało gładko, bez żadnych opóźnień.

DOLNA LINIA

Czy taka zauważalna rozbieżność między wersjami deweloperskimi i produkcyjnymi Reacta jest normalna?

Czy rozumiem / robię coś złego, gdy myślę o tym, jak React zarządza listą?

AKTUALIZACJA 14-11-2016

Znalazłem tę prezentację Michaela Jacksona, w której podejmuje on problem bardzo podobny do tego: https://youtu.be/7S8v8jfLb1Q?t=26m2s

Rozwiązanie jest bardzo podobne do rozwiązania zaproponowanego w odpowiedzi AskarovBeknara poniżej

AKTUALIZACJA 14-4-2018

Ponieważ jest to najwyraźniej popularne pytanie, a od czasu zadania oryginalnego pytania sytuacja się rozwinęła, a zachęcam do obejrzenia wideo, do którego link znajduje się powyżej, w celu zapoznania się z wirtualnym układem, zachęcam również do korzystania z narzędzia React Virtualized biblioteka, jeśli nie chcesz wymyślać koła na nowo.

Dimitris Karagiannis
źródło
Co masz na myśli mówiąc o rozwojowej / produkcyjnej wersji reagowania?
Dibesjr
@Dibesjr facebook.github.io/react/…
Dimitris Karagiannis
Ach, rozumiem, dzięki. Odpowiadając na jedno z pytań, napisano, że istnieje rozbieżność w optymalizacji między wersjami. Jedną rzeczą, na którą należy zwrócić uwagę na dużych listach, jest tworzenie funkcji w renderowaniu. Będzie miał hit wydajności, gdy trafisz na gigantyczne listy. Spróbowałbym sprawdzić, ile czasu zajmie wygenerowanie tej listy za pomocą ich narzędzi perf. Facebook.github.io/react/docs/perf.html
Dibesjr
2
Myślę, że powinieneś ponownie rozważyć użycie Redux, ponieważ jest to dokładnie to, czego potrzebujesz tutaj (lub dowolnego rodzaju implementacji strumienia). Powinieneś definitywnie spojrzeć na tę prezentację: Big List High Performance React & Redux
Pierre Criulanscy
2
Wątpię, czy użytkownik ma jakąkolwiek korzyść z przewijania 10000 wyników. A co, jeśli wyrenderujesz tylko 100 najlepszych wyników i zaktualizujesz je na podstawie zapytania.
Koen.

Odpowiedzi:

18

Podobnie jak w przypadku wielu innych odpowiedzi na to pytanie, główny problem polega na tym, że renderowanie tak wielu elementów w DOM podczas filtrowania i obsługi kluczowych zdarzeń będzie przebiegać wolno.

Nie robisz niczego z natury złego w związku z Reactem, który powoduje problem, ale podobnie jak wiele innych problemów związanych z wydajnością, interfejs użytkownika może również ponosić duży procent winy.

Jeśli Twój interfejs użytkownika nie jest zaprojektowany z myślą o wydajności, ucierpią nawet narzędzia takie jak React, które zostały zaprojektowane z myślą o wydajności.

Filtrowanie zestawu wyników to świetny początek, o czym wspomniał @Koen

Bawiłem się trochę tym pomysłem i stworzyłem przykładową aplikację, która ilustruje, jak mogę zacząć rozwiązywać tego rodzaju problemy.

Nie jest to bynajmniej production readykod, ale dobrze ilustruje koncepcję i można go zmodyfikować, aby był bardziej solidny, zachęcamy do obejrzenia kodu - mam nadzieję, że przynajmniej daje kilka pomysłów ...;)

przykład-dużej-listy-reakcji

wprowadź opis obrazu tutaj

licho
źródło
1
Naprawdę czuję się źle, że muszę wybrać tylko jedną odpowiedź, wydaje się, że wszyscy włożyli w nie wysiłek, ale obecnie jestem na wakacjach bez komputera i nie mogę naprawdę sprawdzić ich z uwagą, na którą zasługują. Wybrałem ten, ponieważ jest wystarczająco krótki i na temat, aby go zrozumieć nawet czytając z telefonu. Kiepski powód, który znam.
Dimitris Karagiannis
Co masz na myśli, mówiąc o edycji pliku hosta 127.0.0.1 * http://localhost:3001?
stackjlei
@stackjlei Myślę, że miał na myśli mapowanie 127.0.0.1 na localhost: 3001 w / etc / hosts
Maverick,
16

Moje doświadczenie z bardzo podobnym problemem jest takie, że reakcja naprawdę cierpi, jeśli w DOM jest więcej niż 100-200 lub więcej komponentów na raz. Nawet jeśli jesteś bardzo ostrożny (ustawiając wszystkie klucze i / lub wdrażając shouldComponentUpdatemetodę), aby zmienić tylko jeden lub dwa komponenty podczas ponownego renderowania, nadal będziesz w świecie zranienia.

Powolną częścią reakcji w tej chwili jest porównanie różnicy między wirtualnym DOM a rzeczywistym DOM. Jeśli masz tysiące komponentów, ale aktualizujesz tylko kilka, nie ma to znaczenia, reakcja wciąż ma ogromną różnicę do wykonania między modelami DOM.

Kiedy teraz piszę strony, staram się projektować je tak, aby zminimalizować liczbę komponentów, jednym ze sposobów na zrobienie tego podczas renderowania dużych list komponentów jest ... cóż ... nie renderowanie dużych list komponentów.

Chodzi mi o to, że: renderuj tylko komponenty, które obecnie widzisz, renderuj więcej podczas przewijania w dół, prawdopodobnie użytkownik nie przewinie w żaden sposób tysięcy komponentów ... Mam nadzieję.

Świetną biblioteką do tego jest:

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

Ze świetnym poradnikiem:

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

Obawiam się, że nie usuwa on jednak komponentów, które znajdują się na górze strony, więc jeśli przewijasz wystarczająco długo, problemy z wydajnością zaczną się ponownie pojawiać.

Wiem, że podanie linku jako odpowiedzi nie jest dobrą praktyką, ale podane przez nich przykłady wyjaśnią, jak korzystać z tej biblioteki znacznie lepiej niż ja tutaj. Mam nadzieję, że wyjaśniłem, dlaczego duże listy są złe, ale także obejść.

Rezonans
źródło
2
Aktualizacja: pakiet znajdujący się w tej odpowiedzi nie jest obsługiwany. Widelec jest ustawiony na npmjs.com/package/react-infinite-scroller
Ali Al Amine
11

Po pierwsze, różnica między wersją deweloperską i produkcyjną Reacta jest ogromna, ponieważ w produkcji jest wiele pomijanych testów poprawności (takich jak weryfikacja typów rekwizytów).

Wtedy myślę, że powinieneś ponownie rozważyć użycie Redux, ponieważ byłoby to niezwykle pomocne w tym, czego potrzebujesz (lub jakiejkolwiek implementacji strumienia). Powinieneś definitywnie przyjrzeć się tej prezentacji: Big List High Performance React & Redux .

Ale zanim zagłębimy się w redux, musisz dokonać pewnych poprawek w swoim kodzie React, dzieląc komponenty na mniejsze komponenty, ponieważ shouldComponentUpdatecałkowicie ominie renderowanie dzieci, więc jest to ogromny zysk .

Gdy masz bardziej szczegółowe komponenty, możesz obsłużyć stan za pomocą redux i re-redux, aby lepiej zorganizować przepływ danych.

Niedawno miałem podobny problem, gdy musiałem wyrenderować tysiąc wierszy i móc modyfikować każdy wiersz, edytując jego zawartość. Ta miniaplikacja wyświetla listę koncertów z potencjalnymi duplikatami koncertów i muszę wybrać dla każdego potencjalnego duplikatu, jeśli chcę oznaczyć potencjalny duplikat jako oryginalny koncert (nie duplikat), zaznaczając pole wyboru i, jeśli to konieczne, edytować nazwa koncertu. Jeśli nic nie zrobię dla konkretnego potencjalnego duplikatu, zostanie on uznany za zduplikowany i zostanie usunięty.

Oto jak to wygląda:

wprowadź opis obrazu tutaj

Istnieją zasadniczo 4 komponenty sieci (jest tutaj tylko jeden rząd, ale to dla przykładu):

wprowadź opis obrazu tutaj

Oto pełny kod (CodePen pracy: Ogromne Lista z React & Redux ) używając Redux , reagują-Redux , niezmienne , Reselect i skomponuj :

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')
);

Wnioski wyciągnięte z wykonania tej miniaplikacji podczas pracy z ogromnym zbiorem danych

  • Komponenty React działają najlepiej, gdy są małe
  • Ponowny wybór stał się bardzo przydatny, aby uniknąć ponownego obliczania i zachować ten sam obiekt odniesienia (w przypadku korzystania z immutable.js) z tymi samymi argumentami.
  • Utwórz connectkomponent ed dla komponentu, który jest najbliższy z potrzebnych danych, aby uniknąć przekazywania komponentu tylko właściwości, których nie używają
  • Użycie funkcji tkaniny do tworzenia mapDispatchToProps, gdy potrzebujesz tylko początkowej właściwości podanej w, ownPropsjest konieczne, aby uniknąć niepotrzebnego ponownego renderowania
  • Reaguj i redukuj definitywnie razem rocka!
Pierre Criulanscy
źródło
2
Nie sądzę, aby dodanie zależności do redux było konieczne, aby rozwiązać problem OP, dalsze dalsze działania wysyłające do filtrowania jego zestawu wyników tylko pogorszyłyby problem, wysyłki nie są tak tanie, jak mogłoby się wydawać, obsługa tej konkretnej sytuacji z komponentem lokalnym stan jest najbardziej wydajnym podejściem
deowk
4
  1. Reakcja w wersji rozwojowej sprawdza właściwości każdego komponentu, aby ułatwić proces rozwoju, podczas gdy w produkcji jest on pomijany.

  2. Filtrowanie listy ciągów jest bardzo kosztowną operacją dla każdego klucza. Może to powodować problemy z wydajnością z powodu jednowątkowego charakteru JavaScript. Rozwiązaniem może być użycie metody debounce , aby opóźnić wykonanie funkcji filtru do momentu wygaśnięcia opóźnienia.

  3. Innym problemem może być sama ogromna lista. Możesz stworzyć wirtualny układ i ponownie wykorzystać utworzone elementy, zastępując dane. Zasadniczo tworzysz przewijalny komponent kontenera o stałej wysokości, wewnątrz którego umieścisz kontener z listą. Wysokość kontenera listy należy ustawić ręcznie (itemHeight * numberOfItems) w zależności od długości widocznej listy, aby pasek przewijania działał. Następnie utwórz kilka komponentów przedmiotów, tak aby wypełniały wysokość przewijalnych kontenerów i być może dodają dodatkowy jeden lub dwa naśladujące ciągły efekt listy. ustaw je w pozycji absolutnej, a po przewinięciu po prostu przesuń ich pozycję, aby naśladowała ciągłą listę (myślę, że dowiesz się, jak to zaimplementować :)

  4. Jeszcze jedno, że pisanie do DOM jest kosztowną operacją, zwłaszcza jeśli zrobisz to źle. Możesz używać kanwy do wyświetlania list i płynnie obsługiwać przewijanie. Komponenty React-Canvas w usłudze Checkout. Słyszałem, że wykonali już trochę pracy nad Listami.

AskarovBeknar
źródło
Jakieś informacje o React in development? i po co sprawdzać prototypy każdego komponentu?
Liuuil
4

Sprawdź React Virtualized Select, został zaprojektowany, aby rozwiązać ten problem i działa imponująco w moim odczuciu. Z opisu:

HOC, który korzysta z funkcji zwirtualizowanych React i Select do wyświetlania dużych list opcji w rozwijanym menu

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

Madbreaks
źródło
4

Jak wspomniałem w moim komentarzu , wątpię, aby użytkownicy potrzebowali tych wszystkich 10000 wyników naraz w przeglądarce.

Co się stanie, jeśli przejrzysz wyniki i zawsze wyświetlasz listę 10 wyników.

Mam stworzony przykład przy użyciu tej techniki, bez pomocy innych bibliotek, takich jak Redux. Obecnie tylko z nawigacją za pomocą klawiatury, ale można go łatwo rozszerzyć również o pracę przy przewijaniu.

Przykład składa się z 3 komponentów: aplikacji kontenera, komponentu wyszukiwania i komponentu listy. Prawie cała logika została przeniesiona do komponentu kontenera.

Kłamstwa GIST w zachowaniu ścieżki starti selectedrezultatu, a przeniesienie tych interakcji klawiatury.

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

Po prostu przepuszczając wszystkie pliki przez filtr:

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

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

I krojenie wyników na podstawie starti limitw rendermetodzie:

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

Skrzypce zawierające pełny działający przykład: https://jsfiddle.net/koenpunt/hm1xnpqk/

Koen.
źródło
3

Wypróbuj filtr przed załadowaniem do komponentu React i pokaż tylko rozsądną liczbę elementów w komponencie i załaduj więcej na żądanie. Nikt nie może wyświetlić jednocześnie tylu elementów.

Myślę, że tak nie jest, ale nie używaj indeksów jako kluczy .

Aby poznać prawdziwy powód, dla którego wersje rozwojowe i produkcyjne są różne, możesz wypróbować profilingswój kod.

Załaduj swoją stronę, rozpocznij nagrywanie, dokonaj zmiany, zatrzymaj nagrywanie, a następnie sprawdź chronometraż. Zobacz tutaj, aby uzyskać instrukcje dotyczące profilowania wydajności w przeglądarce Chrome .

RationalDev lubi GoFundMonica
źródło
2

Dla każdego borykającego się z tym problemem napisałem komponent react-big-list który obsługuje listy do 1 miliona rekordów.

Oprócz tego ma kilka ciekawych dodatkowych funkcji, takich jak:

  • Sortowanie
  • Buforowanie
  • Niestandardowe filtrowanie
  • ...

Używamy go w produkcji w kilku aplikacjach i działa świetnie.

Meemaw
źródło