Knockout.js неймовірно повільний при напіввеликих наборах даних


86

Я тільки починаю роботу з Knockout.js (завжди хотів спробувати, але тепер нарешті маю виправдання!) - Однак я стикаюся з деякими дуже поганими проблемами з продуктивністю, коли прив'язую таблицю до відносно невеликого набору даних (близько 400 рядків або близько того).

У своїй моделі я маю такий код:

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

Проблема полягає в тому, що forцикл вище займає близько 30 секунд або близько того, приблизно 400 рядків. Однак, якщо я зміню код на:

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

Потім forпетля завершується у мить ока. Іншими словами, pushметод об'єкта "Нокаут" observableArrayнеймовірно повільний.

Ось мій шаблон:

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

Мої запитання:

  1. Це правильний спосіб прив’язати мої дані (які походять від методу AJAX) до спостережуваної колекції?
  2. Я сподіваюся push, робить важкий повторний розрахунок кожного разу, коли я його викликаю, наприклад, можливо, відновлює пов'язані об'єкти DOM. Чи є спосіб або затримати цей повторний повтор, або, можливо, всунути всі мої предмети відразу?

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

ОНОВЛЕННЯ:

Відповідно до наведених нижче порад, я оновив свій код:

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

Однак this.projects()для 400 рядків все одно потрібно близько 10 секунд. Зізнаюся, я не впевнений, наскільки швидко це було б без Knockout (просто додавання рядків через DOM), але я маю відчуття, що це було б набагато швидше, ніж 10 секунд.

ОНОВЛЕННЯ 2:

Згідно з іншою порадою нижче, я дав jQuery.tmpl постріл (який спочатку підтримується KnockOut), і цей механізм шаблонування намалює близько 400 рядків лише за 3 секунди. Це здається найкращим підходом, за винятком рішення, яке динамічно завантажує більше даних під час прокрутки.


1
Чи використовуєте ви прив'язку нокаута foreach або прив'язку шаблону з foreach. Мені просто цікаво, чи може використання шаблону та включення jquery tmpl замість власного механізму шаблонів змінити ситуацію.
madcapnmckay

1
@MikeChristensen - Knockout має власний механізм власних шаблонів, пов'язаний із прив'язками (foreach, with). Він також підтримує інші механізми шаблонів, а саме jquery.tmpl. Докладніше читайте тут . Я не робив ніяких тестів з різними двигунами, тому не знаю, чи це допоможе. Читаючи ваш попередній коментар, у IE7 ви можете боротися за те, щоб досягти ефективності, яку вам потрібно.
madcapnmckay

2
Враховуючи, що ми щойно отримали IE7 кілька місяців тому, я думаю, що IE9 буде випущений приблизно влітку 2019 року. О, ми всі теж на WinXP .. Blech.
Майк Крістенсен,

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

1
Я знайшов спосіб, який є швидшим та акуратнішим (нічого нестандартного). використання valueHasMutatedробить це. перевірте відповідь, якщо у вас є час.
супер круто

Відповіді:


16

Як пропонується в коментарях.

Knockout має власний механізм власних шаблонів, пов'язаний із прив'язками (foreach, with). Він також підтримує інші механізми шаблонів, а саме jquery.tmpl. Докладніше читайте тут . Я не робив ніяких тестів з різними двигунами, тому не знаю, чи це допоможе. Читаючи ваш попередній коментар, у IE7 ви можете боротися за те, щоб досягти ефективності, яку вам потрібно.

Крім того, KO підтримує будь-який механізм шаблонування js, якщо хтось написав адаптер для нього, який є. Можливо, ви захочете спробувати інших, оскільки jquery tmpl повинен бути замінений на JsRender .


Я стаю набагато кращим, jquery.tmplтому буду використовувати це. Я можу дослідити інші двигуни, а також написати свій власний, якщо у мене буде зайвий час. Дякую!
Майк Крістенсен

1
@MikeChristensen - ви все ще використовуєте data-bindоператори у своєму шаблоні jQuery, чи використовуєте синтаксис $ {code}?
ericb

@ericb - З новим кодом я використовую ${code}синтаксис, і це набагато швидше. Я також намагався змусити Underscore.js працювати, але ще мені не пощастило ( <% .. %>синтаксис заважає ASP.NET), і, схоже, ще немає підтримки JsRender.
Майк Крістенсен

1
@MikeChristensen - добре, тоді це має сенс. Механізм власних шаблонів KO не обов’язково настільки неефективний. Коли ви використовуєте синтаксис $ {code}, ви не отримуєте прив'язки даних до цих елементів (що покращує ефективність). Таким чином, якщо ви зміните властивість a ResultRow, він не оновить інтерфейс користувача (вам доведеться оновити projectsobservableArray, що змусить повторно відтворити вашу таблицю). $ {} однозначно може бути вигідним, якщо ваші дані в значній
мірі доступні

4
Некромантія! jquery.tmpl більше не розробляється
Alex Larzelere

50

Будь ласка, дивіться: Knockout.js Performance Gotcha # 2 - Маніпулювання спостережуваними масивами

Кращий шаблон - отримати посилання на наш базовий масив, натиснути на нього, а потім викликати .valueHasMutated (). Тепер наші абоненти отримають лише одне повідомлення про те, що масив змінився.


13

Використовуйте пагінацію з KO на додаток до $ .map.

У мене була та ж проблема з великим набором даних із 1400 записів, поки я не використовував пейджинговий з нокаутом. Використовуючи$.map для завантаження записів мало величезну різницю, але час візуалізації DOM все ще був жахливим. Потім я спробував використати пагінацію, і це зробило моє освітлення набору даних швидким, так само як і більш зручним для користувачів. Розмір сторінки 50 зробив набір даних набагато менш вражаючим і різко зменшив кількість елементів DOM.

Це дуже легко зробити з KO:

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


11

У KnockoutJS є декілька чудових підручників, особливо про завантаження та збереження даних

У їхньому випадку вони getJSON()отримують дані, використовуючи надзвичайно швидко. З їхнього прикладу:

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

1
Безумовно, це значне покращення, але self.tasks(mappedTasks)на запуск потрібно близько 10 секунд (з 400 рядками). Я вважаю, що це все ще не прийнятно.
Майк Крістенсен

Погоджуся, що 10 секунд неприйнятно. Використовуючи knockoutjs, я не впевнений, що краще, ніж карта, тому я вибрав це запитання і подивився на кращу відповідь.
deltree

1
В порядку. Відповідь, безумовно, заслуговує +1як на спрощення мого коду, так і на різке збільшення швидкості. Можливо, хтось має більш детальне пояснення, що таке вузьке місце.
Майк Крістенсен

9

Дайте KoGrid вид. Він інтелектуально управляє рендерингом рядків, щоб він був більш продуктивним.

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

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

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

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


1
Здається, це повністю пошкоджено на IE7 (жоден зразок не працює), інакше це було б чудово!
Майк Крістенсен

Радий вивчити це - KoGrid все ще активно розвивається. Однак, це, принаймні, відповідає на ваше запитання стосовно перфу?
ericb

1
Так! Це підтверджує мою початкову підозру, що механізм шаблонів KO за замовчуванням працює досить повільно. Якщо вам потрібен хтось для морської свинки KoGrid для вас, я був би радий. Звучить саме те, що нам потрібно!
Mike Christensen

Проклятий. Це виглядає дуже добре! На жаль, понад 50% користувачів мого додатка використовують IE7!
Джим Г.

Цікаво, що в наш час ми неохоче підтримуємо IE11. Справи покращились за останні 7 років.
MrBoJangles

5

Рішення, щоб уникнути блокування браузера під час рендерингу дуже великого масиву, полягає в тому, щоб масив "гасився" таким чином, щоб одночасно додавались лише кілька елементів, а між ними був сон. Ось функція, яка буде робити саме це:

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

Залежно від випадку використання, це може призвести до значного вдосконалення UX, оскільки користувач може бачити лише першу партію рядків, перш ніж прокручувати.


Мені подобається це рішення, але замість setTimeout на кожній ітерації я рекомендую запускати setTimout кожні 20 і більше ітерацій, оскільки кожен час завантажується занадто довго. Я бачу, що ви робите це з +20, але для мене це було не очевидно на перший погляд.
charlierlee

5

Користуючись перевагами аргументів змінної push (), я отримав найкращі результати у моєму випадку. 1300 рядків завантажувалися протягом 5973 мс (~ 6 сек.). З цією оптимізацією час навантаження зменшився до 914 мс (<1 сек.)
Це покращення на 84,7%!

Більше інформації на сторінці Переміщення елементів до спостережуваного масиву

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

4

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

Модель перегляду:

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) 

Після виклику (4)дані масиву будуть завантажені в необхідний ObservableArray, який єthis.projects автоматично.

якщо у вас є час, подивіться на це і про всяк випадок будь-яких проблем повідомте мене

Хитрість тут: Роблячи так, якщо у випадку виникнення будь-яких залежностей (обчислюється, підписується тощо) можна уникнути на рівні натискання, і ми можемо змусити їх виконати за один раз після дзвінка (4).


1
Проблема не в тому, що надто багато дзвінків push, проблема полягає в тому, що навіть один дзвінок для натискання призведе до тривалого часу рендерингу. Якщо масив містить 1000 елементів, прив'язаних до a foreach, натискання одного елемента відображає весь foreach, і ви платите великі витрати часу на рендеринг.
Невеликий

1

Можливе обхідне рішення, в поєднанні з використанням jQuery.tmpl, полягає в тому, щоб асинхронно надсилати елементи одночасно до спостережуваного масиву, використовуючи 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
  }
}

Таким чином, коли ви додаєте лише один елемент за раз, браузер / knockout.js може не поспішати маніпулювати DOM відповідним чином, не переглядаючи браузер повністю блокуванням протягом декількох секунд, так що користувач може одночасно прокручувати список.


2
Це призведе до N кількості оновлень DOM, що призведе до загального часу візуалізації, який набагато довший, ніж виконувати все одразу.
Fredrik C

Це, звичайно, правильно. Сенс, однак, полягає в тому, що поєднання N, яке є великим числом, і введення елемента в масив проектів, що викликає значну кількість інших оновлень або обчислень DOM, може призвести до того, що браузер зависне і запропонує вам вбити вкладку. Отримавши тайм-аут, або для елемента, або для 10, 100 або якоїсь іншої кількості елементів, браузер все одно реагуватиме.
gnab

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

1
Звичайно, це неправильний підхід у загальному випадку, і ніхто не погодиться з вами у цьому. Це хак і доказ концепції для запобігання зависанню браузера, якщо вам потрібно робити багато операцій DOM. Мені це знадобилося ще пару років тому, коли було перераховано кілька великих таблиць HTML з декількома прив'язками на клітинку, в результаті чого оцінювалися тисячі прив'язок, кожна з яких впливала на стан DOM. Функціонал потрібен був тимчасово для перевірки правильності повторної реалізації настільної програми на базі Excel як веб-програми. Тоді це рішення працювало ідеально.
gnab

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

1

Я експериментував з продуктивністю і маю два внески, які, мабуть, можуть бути корисними.

Мої експерименти спрямовані на час маніпулювання DOM. Тому перед тим, як перейти до цього, безумовно варто дотримуватися наведених вище пунктів про введення масиву JS перед створенням спостережуваного масиву тощо.

Але якщо час маніпуляцій DOM все ще перешкоджає вам, то це може допомогти:


1: Шаблон, щоб обернути завантажувальний блешню навколо повільного візуалізації, а потім приховати його за допомогою afterRender

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

Це насправді не вирішує проблему продуктивності, але показує, що затримка, мабуть, неминуча, якщо ви перебираєте тисячі елементів, і вона використовує шаблон, за допомогою якого ви можете переконатися, що перед довгою операцією КО з’явиться завантажувач, а потім приховати це згодом. Таким чином, це покращує UX, принаймні.

Переконайтеся, що ви можете завантажити блешню:

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

Сховати блешню:

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

що запускає:

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

2: Використання прив'язки html як зламу

Я згадав стару техніку ще з часів, коли працював над приставкою з Opera, будуючи інтерфейс користувача за допомогою DOM-маніпуляцій. Це було жахливо повільно, тому рішенням було зберігати великі фрагменти HTML як рядки та завантажувати рядки, встановлюючи властивість innerHTML.

Щось подібне можна досягти, використовуючи прив'язку html і обчислювальну форму, яка отримує HTML для таблиці як великий шматок тексту, а потім застосовує його одним рухом. Це справді вирішує проблему продуктивності, але суттєвим недоліком є ​​те, що він суворо обмежує те, що ви можете зробити із прив'язкою всередині кожного рядка таблиці.

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

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


1

Якщо ви використовуєте IE, спробуйте закрити інструменти розробника.

Відкриття інструментів розробника в IE значно уповільнює цю операцію. Я додаю до масиву ~ 1000 елементів. Коли інструменти розробника відкриті, це займає близько 10 секунд, і IE зависає, поки це відбувається. Коли я закриваю інструменти розробника, операція миттєва, і я не бачу уповільнення роботи в IE.


0

Я також помітив, що движок шаблонів Knockout js працює повільніше в IE, я замінив його на underscore.js, працює набагато швидше.


Як ти це зробив, будь ласка?
Стю Харпер

@StuHarper Я імпортував бібліотеку підкреслення, а потім у main.js дотримався кроків, описаних розділом інтеграції підкреслення knockoutjs.com/documentation/template-binding.html
Марчелло,

З якою версією IE відбулося це вдосконалення?
bkwdesign

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