Knockout.js jest niesamowicie powolny w przypadku średnio dużych zbiorów danych

86

Dopiero zaczynam pracę z Knockout.js (zawsze chciałem go wypróbować, ale teraz w końcu mam wymówkę!) - Jednak napotykam naprawdę złe problemy z wydajnością, gdy wiążę tabelę ze stosunkowo małym zestawem dane (około 400 wierszy).

W moim modelu mam następujący kod:

this.projects = ko.observableArray( [] ); //Bind to empty array at startup

this.loadData = function (data) //Called when AJAX method returns
{
   for(var i = 0; i < data.length; i++)
   {
      this.projects.push(new ResultRow(data[i])); //<-- Bottleneck!
   }
};

Problem polega na tym, że forpowyższa pętla zajmuje około 30 sekund z około 400 rzędami. Jeśli jednak zmienię kod na:

this.loadData = function (data)
{
   var testArray = []; //<-- Plain ol' Javascript array
   for(var i = 0; i < data.length; i++)
   {
      testArray.push(new ResultRow(data[i]));
   }
};

Następnie forpętla kończy się w mgnieniu oka. Innymi słowy, pushmetoda obiektu Knockout observableArrayjest niesamowicie powolna.

Oto mój szablon:

<tbody data-bind="foreach: projects">
    <tr>
       <td data-bind="text: code"></td>
       <td><a data-bind="projlink: key, text: projname"></td>
       <td data-bind="text: request"></td>
       <td data-bind="text: stage"></td>
       <td data-bind="text: type"></td>
       <td data-bind="text: launch"></td>
       <td><a data-bind="mailto: ownerEmail, text: owner"></a></td>
    </tr>
</tbody>

Moje pytania:

  1. Czy to właściwy sposób na powiązanie moich danych (pochodzących z metody AJAX) z obserwowalną kolekcją?
  2. Spodziewam się, że za pushkażdym razem, gdy to nazywam, wykonuję ciężkie ponowne obliczenia, na przykład może odbudować powiązane obiekty DOM. Czy istnieje sposób, aby opóźnić to ponowne obliczenie lub wrzucić wszystkie moje elementy jednocześnie?

W razie potrzeby mogę dodać więcej kodu, ale jestem prawie pewien, że jest to istotne. Przez większość czasu po prostu śledziłem samouczki Knockout z witryny.

AKTUALIZACJA:

Zgodnie z poniższą radą zaktualizowałem swój kod:

this.loadData = function (data)
{
   var mappedData = $.map(data, function (item) { return new ResultRow(item) });
   this.projects(mappedData);
};

Jednak this.projects()nadal trwa około 10 sekund dla 400 wierszy. Przyznaję, że nie jestem pewien, jak szybko byłoby to bez Knockout (po prostu dodając wiersze przez DOM), ale mam wrażenie, że potrwa to znacznie szybciej niż 10 sekund.

AKTUALIZACJA 2:

Zgodnie z innymi wskazówkami poniżej, dałem ujęcie jQuery.tmpl (który jest natywnie obsługiwany przez KnockOut), a ten silnik szablonów narysuje około 400 wierszy w nieco ponad 3 sekundy. Wydaje się, że jest to najlepsze podejście, bez rozwiązania, które dynamicznie ładowałoby więcej danych podczas przewijania.

Mike Christensen
źródło
1
Czy używasz wiązania typu knockout foreach, czy wiązania szablonu z foreach. Zastanawiam się tylko, czy użycie szablonu i włączenie jquery tmpl zamiast natywnego silnika szablonów może coś zmienić.
madcapnmckay
1
@MikeChristensen - Knockout ma własny natywny silnik szablonów powiązany z powiązaniami (foreach, with). Obsługuje również inne silniki szablonów, a mianowicie jquery.tmpl. Więcej informacji znajdziesz tutaj . Nie wykonałem żadnego testu porównawczego z różnymi silnikami, więc nie wiem, czy to pomoże. Czytając swój poprzedni komentarz, w IE7 możesz mieć trudności z uzyskaniem żądanej wydajności.
madcapnmckay
2
Biorąc pod uwagę, że właśnie dostaliśmy IE7 kilka miesięcy temu, myślę, że IE9 zostanie wydany około lata 2019 roku. Och, wszyscy też korzystamy z WinXP ... Blech.
Mike Christensen
1
ps, Powodem, dla którego wydaje się to powolne, jest to, że dodajesz 400 elementów do tej obserwowalnej tablicy indywidualnie . Przy każdej zmianie obserwowalnego widok musi zostać ponownie wyrenderowany na wszystko, co zależy od tej tablicy. W przypadku złożonych szablonów i wielu elementów do dodania to dużo narzutów, gdy wystarczyłoby zaktualizować tablicę od razu, ustawiając ją na inną instancję. Przynajmniej wtedy ponowne renderowanie nastąpi raz.
Jeff Mercado,
1
Znalazłem sposób, który jest szybszy i schludny (nic nietypowego). używając valueHasMutatedrobi to. sprawdź odpowiedź, jeśli masz czas.
super fajne

Odpowiedzi:

16

Jak zasugerowano w komentarzach.

Knockout ma własny natywny silnik szablonów powiązany z powiązaniami (foreach, with). Obsługuje również inne silniki szablonów, a mianowicie jquery.tmpl. Więcej informacji znajdziesz tutaj . Nie wykonałem żadnego testu porównawczego z różnymi silnikami, więc nie wiem, czy to pomoże. Czytając swój poprzedni komentarz, w IE7 możesz mieć trudności z uzyskaniem żądanej wydajności.

Na marginesie, KO obsługuje dowolny silnik szablonu js, jeśli ktoś napisał do niego adapter. Możesz wypróbować inne, ponieważ jquery tmpl ma zostać zastąpione przez JsRender .

madcapnmckay
źródło
Dzięki temu osiągam znacznie lepszą wydajność, jquery.tmplwięc użyję tego. Mogę zbadać inne silniki, a także napisać własne, jeśli będę miał trochę więcej czasu. Dzięki!
Mike Christensen
1
@MikeChristensen - czy nadal używasz data-bindinstrukcji w swoim szablonie jQuery, czy też używasz składni $ {code}?
ericb
@ericb - W nowym kodzie używam ${code}składni i jest znacznie szybsza. Próbowałem również uruchomić Underscore.js, ale nie miałem jeszcze szczęścia ( <% .. %>składnia koliduje z ASP.NET) i wydaje się, że nie ma jeszcze wsparcia dla JsRender.
Mike Christensen
1
@MikeChristensen - ok, to ma sens. Natywny silnik szablonów KO niekoniecznie jest tak nieefektywny. Kiedy używasz składni $ {code}, nie otrzymujesz żadnego powiązania danych z tymi elementami (co poprawia perf). Dlatego jeśli zmienisz właściwość a ResultRow, nie zaktualizuje to interfejsu użytkownika (będziesz musiał zaktualizować projectsobserwowalną tablicę, co wymusi ponowne renderowanie tabeli). $ {} może być zdecydowanie korzystne, jeśli twoje dane są prawie tylko do odczytu
ericb
4
Nekromancja! jquery.tmpl nie jest już w fazie rozwoju
Alex Larzelere
13

Użyj paginacji z KO oprócz używania $ .map.

Miałem ten sam problem z dużymi zbiorami danych obejmującymi 1400 rekordów, dopóki nie użyłem stronicowania z nokautem. Używanie $.mapdo ładowania rekordów robiło ogromną różnicę, ale czas renderowania DOM był nadal ohydny. Potem spróbowałem użyć paginacji, co sprawiło, że mój zbiór danych świecił szybko i był bardziej przyjazny dla użytkownika. Rozmiar strony wynoszący 50 sprawił, że zestaw danych był znacznie mniej przytłaczający i radykalnie zmniejszył liczbę elementów DOM.

Bardzo łatwo to zrobić z KO:

http://jsfiddle.net/rniemeyer/5Xr2X/

Tim Santeford
źródło
11

KnockoutJS ma kilka świetnych samouczków, szczególnie ten dotyczący ładowania i zapisywania danych

W ich przypadku pobierają dane, getJSON()które są niezwykle szybkie. Z ich przykładu:

function TaskListViewModel() {
    // ... leave the existing code unchanged ...

    // Load initial state from server, convert it to Task instances, then populate self.tasks
    $.getJSON("/tasks", function(allData) {
        var mappedTasks = $.map(allData, function(item) { return new Task(item) });
        self.tasks(mappedTasks);
    });    
}
deltree
źródło
1
Zdecydowanie duża poprawa, ale uruchomienie self.tasks(mappedTasks)zajmuje około 10 sekund (przy 400 rzędach). Uważam, że jest to nadal nie do zaakceptowania.
Mike Christensen
Zgadzam się, że 10 sekund to niedopuszczalne. Używając knockoutjs, nie jestem pewien, co jest lepsze niż mapa, więc polubię to pytanie i czekam na lepszą odpowiedź.
deltree
1
Ok. Odpowiedź zdecydowanie zasługuje na to, +1aby zarówno uprościć mój kod, jak i radykalnie zwiększyć szybkość. Być może ktoś ma bardziej szczegółowe wyjaśnienie, czym jest wąskie gardło.
Mike Christensen
9

Daj KoGrid wygląd. Inteligentnie zarządza renderowaniem wierszy, dzięki czemu jest bardziej wydajny.

Jeśli próbujesz powiązać 400 wierszy z tabelą za pomocą foreachwiązania, będziesz miał problem z przepchnięciem tego przez KO do DOM.

KO robi kilka bardzo interesujących rzeczy przy użyciu foreachwiązania, z których większość to bardzo dobre operacje, ale zaczynają się rozpadać na perf, gdy rozmiar twojej tablicy rośnie.

Przeszedłem długą, ciemną drogę, próbując powiązać duże zestawy danych z tabelami / siatkami, a ty w końcu musisz rozdzielić / stronicować dane lokalnie.

KoGrid robi to wszystko. Został zbudowany, aby renderować tylko te wiersze, które widz może zobaczyć na stronie, a następnie wirtualizować inne wiersze, dopóki nie będą potrzebne. Myślę, że przekonasz się, że jego wydajność w 400 przedmiotach jest znacznie lepsza niż doświadczasz.

ericb
źródło
1
Wygląda na to, że w IE7 jest całkowicie zepsuty (żadna z próbek nie działa), w przeciwnym razie byłoby świetnie!
Mike Christensen
Miło mi się temu przyjrzeć - KoGrid jest nadal w fazie aktywnego rozwoju. Czy to przynajmniej odpowiada na twoje pytanie dotyczące perf?
ericb
1
Tak! Potwierdza moje pierwotne podejrzenie, że domyślny silnik szablonów KO działa dość wolno. Jeśli potrzebujesz kogoś do świnki morskiej KoGrid dla Ciebie, byłbym szczęśliwy. Brzmi jak dokładnie to, czego potrzebujemy!
Mike Christensen
Cerować. To wygląda naprawdę dobrze! Niestety ponad 50% użytkowników mojej aplikacji korzysta z IE7!
Jim G.
Co ciekawe, w dzisiejszych czasach niechętnie musimy wspierać IE11. Sytuacja poprawiła się w ciągu ostatnich 7 lat.
MrBoJangles
5

Rozwiązaniem pozwalającym uniknąć blokowania przeglądarki podczas renderowania bardzo dużej tablicy jest „dławienie” tablicy w taki sposób, że tylko kilka elementów jest dodawanych naraz, z zaśnięciem pomiędzy. Oto funkcja, która to zrobi:

function throttledArray(getData) {
    var showingDataO = ko.observableArray(),
        showingData = [],
        sourceData = [];
    ko.computed(function () {
        var data = getData();
        if ( Math.abs(sourceData.length - data.length) / sourceData.length > 0.5 ) {
            showingData = [];
            sourceData = data;
            (function load() {
                if ( data == sourceData && showingData.length != data.length ) {
                    showingData = showingData.concat( data.slice(showingData.length, showingData.length + 20) );
                    showingDataO(showingData);
                    setTimeout(load, 500);
                }
            })();
        } else {
            showingDataO(showingData = sourceData = data);
        }
    });
    return showingDataO;
}

W zależności od przypadku użycia może to spowodować ogromną poprawę UX, ponieważ użytkownik może zobaczyć tylko pierwszą partię wierszy, zanim będzie musiał przewijać.

teh_senaus
źródło
Podoba mi się to rozwiązanie, ale zamiast setTimeout przy każdej iteracji zalecam uruchamianie setTimout tylko co 20 lub więcej iteracji, ponieważ za każdym razem ładowanie trwa zbyt długo. Widzę, że robisz to z +20, ale na pierwszy rzut oka nie było to dla mnie oczywiste.
charlierlee
5

Wykorzystanie metody push () akceptującej zmienne argumenty dało najlepsze wyniki w moim przypadku. 1300 wierszy ładowano przez 5973 ms (~ 6 sek.). Dzięki tej optymalizacji czas ładowania spadł do 914 ms (<1 s).
To 84,7% poprawy!

Więcej informacji na temat przesyłania elementów do obserowalnej tablicy

this.projects = ko.observableArray( [] ); //Bind to empty array at startup

this.loadData = function (data) //Called when AJAX method returns
{
   var arrMappedData = ko.utils.arrayMap(data, function (item) {
       return new ResultRow(item);
   });
   //take advantage of push accepting variable arguments
   this.projects.push.apply(this.projects, arrMappedData);
};
mitaka
źródło
4

Miałem do czynienia z tak ogromnymi ilościami danych, które napływały do ​​mnie valueHasMutatedjak marzenie.

Zobacz model:

this.projects([]); //make observableArray empty --(1)

var mutatedArray = this.projects(); -- (2)

this.loadData = function (data) //Called when AJAX method returns
{
ko.utils.arrayForEach(data,function(item){
    mutatedArray.push(new ResultRow(item)); -- (3) // push to the array(normal array)  
});  
};
 this.projects.valueHasMutated(); -- (4) 

Po wywołaniu (4)tablicy dane zostaną załadowane do wymaganej tablicy obserwaable, która jest this.projectsautomatycznie.

jeśli masz czas, spójrz na to i na wszelki wypadek daj mi znać

Sztuczka: robiąc to w ten sposób, jeśli w przypadku jakichkolwiek zależności (obliczonych, subskrybowanych itp.) Można uniknąć na poziomie wypychania i możemy sprawić, by były wykonywane za jednym razem po wywołaniu (4).

Super fajne
źródło
1
Problem nie dotyczy zbyt wielu wywołań push, problem polega na tym, że nawet pojedyncze wywołanie push spowoduje długie czasy renderowania. Jeśli tablica ma 1000 elementów powiązanych z a foreach, wypchnięcie pojedynczego elementu powoduje ponowne renderowanie całego elementu foreach, co wiąże się z dużymi kosztami czasu renderowania.
Lekko
1

Możliwym obejściem tego problemu w połączeniu z użyciem jQuery.tmpl jest wypychanie elementów naraz do obserwowalnej tablicy w sposób asynchroniczny, przy użyciu metody setTimeout;

var self = this,
    remaining = data.length;

add(); // Start adding items

function add() {
  self.projects.push(data[data.length - remaining]);

  remaining -= 1;

  if (remaining > 0) {
    setTimeout(add, 10); // Schedule adding any remaining items
  }
}

W ten sposób, gdy dodajesz tylko jeden element na raz, browser / knockout.js może zająć trochę czasu, aby odpowiednio zmodyfikować DOM, bez całkowitego blokowania przeglądarki przez kilka sekund, tak aby użytkownik mógł jednocześnie przewijać listę.

gnab
źródło
2
Wymusi to liczbę N aktualizacji DOM, co spowoduje całkowity czas renderowania, który jest znacznie dłuższy niż robienie wszystkiego na raz.
Fredrik C
To jest oczywiście poprawne. Chodzi jednak o to, że kombinacja N będącej dużą liczbą i umieszczenie elementu w tablicy projektów, wyzwalając znaczną liczbę innych aktualizacji DOM lub obliczeń, może spowodować zawieszenie się przeglądarki i zaproponowanie zabicia karty. Po przekroczeniu limitu czasu na element lub na 10, 100 lub inną liczbę elementów przeglądarka nadal będzie reagować.
gnab
2
Powiedziałbym, że jest to złe podejście w ogólnym przypadku, w którym całkowita aktualizacja nie zamroziłaby przeglądarki, ale jest czymś, czego można użyć, gdy wszystkie inne zawodzą. Dla mnie brzmi to jak źle napisana aplikacja, w której należy rozwiązać problemy z wydajnością, a nie tylko sprawić, by się nie zawiesił.
Fredrik C
1
Oczywiście w ogólnym przypadku jest to niewłaściwe podejście, nikt by się z tobą nie nie zgodził. To hack i weryfikacja koncepcji zapobiegania zamrożeniu przeglądarki, jeśli musisz wykonać wiele operacji DOM. Potrzebowałem tego kilka lat temu, kiedy wymieniałem kilka dużych tabel HTML z kilkoma powiązaniami na komórkę, co skutkowało oceną tysięcy powiązań, z których każde miało wpływ na stan DOM. Funkcjonalność była potrzebna tymczasowo, aby zweryfikować poprawność ponownej implementacji aplikacji klasycznej opartej na Excelu jako aplikacji internetowej. Wtedy to rozwiązanie zadziałało idealnie.
gnab
Komentarz był głównie do przeczytania przez innych, aby nie zakładać, że jest to preferowany sposób. Zakładałem, że wiesz, co robisz.
Fredrik C
1

Eksperymentowałem z wydajnością i mam dwa wkłady, które, mam nadzieję, mogą być przydatne.

Moje eksperymenty skupiają się na czasie manipulacji DOM. Dlatego przed przystąpieniem do tego zdecydowanie warto postępować zgodnie z powyższymi punktami dotyczącymi wypychania do tablicy JS przed utworzeniem obserwowalnej tablicy itp.

Ale jeśli czas manipulacji DOM wciąż przeszkadza, może to pomóc:


1: wzór do zawijania pokrętła ładowania wokół powolnego renderowania, a następnie ukrycia go za pomocą funkcji afterRender

http://jsfiddle.net/HBYyL/1/

To nie jest naprawa problemu z wydajnością, ale pokazuje, że opóźnienie jest prawdopodobnie nieuniknione, jeśli zapętlasz tysiące elementów i używa wzorca, w którym możesz upewnić się, że pojawi się spinner ładowania przed długą operacją KO, a następnie ukryj to później. Więc przynajmniej poprawia UX.

Upewnij się, że możesz załadować spinner:

// Show the spinner immediately...
$("#spinner").show();

// ... by using a timeout around the operation that causes the slow render.
window.setTimeout(function() {
    ko.applyBindings(vm)  
}, 1)

Ukryj spinner:

<div data-bind="template: {afterRender: hide}">

który wyzwala:

hide = function() {
    $("#spinner").hide()
}

2: Używanie powiązania HTML jako sztuczki

Przypomniałem sobie starą technikę z czasów, gdy pracowałem nad dekoderem z Operą, budując interfejs użytkownika za pomocą manipulacji DOM. To było przerażająco wolne, więc rozwiązaniem było przechowywanie dużych fragmentów kodu HTML jako ciągów i ładowanie ciągów przez ustawienie właściwości innerHTML.

Coś podobnego można osiągnąć, używając powiązania html i obliczenia, które wyprowadza kod HTML dla tabeli jako duży fragment tekstu, a następnie stosuje go za jednym razem. To rozwiązuje problem z wydajnością, ale ogromną wadą jest to, że poważnie ogranicza to, co można zrobić z wiązaniem w każdym wierszu tabeli.

Oto skrzypce, które pokazują to podejście, wraz z funkcją, którą można wywołać z wnętrza wierszy tabeli, aby usunąć element w niejasny sposób. Oczywiście nie jest to tak dobre, jak właściwe KO, ale jeśli naprawdę potrzebujesz niesamowitej (ish) wydajności, jest to możliwe obejście.

http://jsfiddle.net/9ZF3g/5/

piątek
źródło
1

Jeśli używasz IE, spróbuj zamknąć narzędzia deweloperskie.

Otwarcie narzędzi programistycznych w IE znacznie spowalnia tę operację. Dodaję ~ 1000 elementów do tablicy. Otwarcie narzędzi deweloperskich trwa około 10 sekund, a przeglądarka IE zawiesza się, gdy to się dzieje. Kiedy zamykam narzędzia programistyczne, operacja jest natychmiastowa i nie widzę spowolnienia w IE.

Jon List
źródło
0

Zauważyłem też, że silnik szablonów Knockout js działa wolniej w IE, zastąpiłem go podkreśleniem.js, działa znacznie szybciej.

Marcello
źródło
Jak ci się podobało?
Stu Harper
@StuHarper Zaimportowałem bibliotekę podkreślenia, a następnie w main.js wykonałem kroki opisane w sekcji integracji podkreślenia knockoutjs.com/documentation/template-binding.html
Marcello
W której wersji IE wystąpiło to ulepszenie?
bkwdesign
@bkwdesign Używałem IE 10, 11.
Marcello