Нокаут.js невероятно медленный при полу-больших наборах данных


Я только начинаю с нокаутом.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 метод объект невероятно медленный.

вот мой шаблон:

<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 делает некоторые тяжелые re-calc каждый раз, когда я называю это, такие как, возможно, восстановление связанных объектов DOM. Есть ли способ либо отложить этот пересчет, либо, возможно, нажать все мои предметы сразу?

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

обновление:

согласно приведенному ниже совету, я обновил свой код:

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

однако, this.projects() все еще занимает около 10 секунд для 400 строки. Я признаю, что я не уверен, как быстро это будет без нокаут (просто добавление строк через DOM), но у меня есть ощущение, что это будет намного быстрее, чем 10 секунд.

обновление 2:

за другие советы ниже, я дал jQuery.tmpl выстрел (который изначально поддерживается нокаутом), и этот шаблонный движок будет рисовать около 400 строк всего за 3 секунды. Это кажется лучшим подходом, за исключением решения это будет динамически загружать больше данных при прокрутке.

12 82

12 ответов:

как предлагается в комментариях.

Knockout имеет собственный собственный механизм шаблонов, связанный с привязками (foreach, with). Он также поддерживает другие шаблонные движки, а именно jquery.тмпл. Читайте здесь для более подробной информации. Я не делал никакого бенчмаркинга с разными двигателями, поэтому не знаю, поможет ли это. Читая ваш предыдущий комментарий, в IE7 вы можете бороться, чтобы получить производительность, которую вы ищете.

кроме того, ко поддерживает какие-либо JS шаблонизатор двигатель, если кто-то написал адаптер для нее есть. Вы можете попробовать другие там, как jquery tmpl должен быть заменен на JsRender.

смотрите: нокаут.JS Performance Gotcha #2-манипулирование observableArrays

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

использовать разбиение на страницы С KO в дополнение к использованию $.карта.

У меня была такая же проблема с большими наборами данных из 1400 записей, пока я не использовал пейджинг с нокаутом. Используя $.map загрузка записей действительно имела огромное значение, но время рендеринга DOM все еще было отвратительным. Затем я попытался использовать разбиение на страницы, и это сделало мой dataset lighting быстрым, а также более удобным для пользователя. Размер страницы 50 сделал набор данных намного менее подавляющим и уменьшил количество элементов DOM драматический.

его очень легко сделать с KO:

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

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

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

Если вы пытаетесь привязать 400 строк таблицы с помощью foreach привязка, у вас будут проблемы с тем, чтобы протолкнуть это через KO в DOM.

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

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

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

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

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, так как пользователь может видеть только первую партию строк перед прокруткой.

использование push () принятие переменных аргументов дало лучшую производительность в моем случае. 1300 строк загружались в течение 5973ms (~6 сек.). При такой оптимизации время загрузки сократилось до 914 МС ( Это 84.7 % улучшение!

более подробная информация Pushing items to an observableArray

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

Я имел дело с такими огромными объемами данных, поступающих в для меня 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).

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

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

но если время манипуляции DOM все еще мешает вам, то это может помочь:


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

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

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

убедитесь, что вы можете загрузить 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)

скрыть блесны:

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

какие триггеры:

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

2: Использование привязки html в качестве взлома

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

что-то подобное может быть достигается с помощью привязки html и вычисляется, что получает HTML для таблицы в виде большого куска текста, а затем применяет его на одном дыхании. Это устраняет проблему производительности, но массовый недостаток заключается в том, что он серьезно ограничивает то, что вы можете сделать с привязкой внутри каждой строки таблицы.

вот скрипка, которая показывает этот подход, вместе с функцией, которая может быть вызвана изнутри строк таблицы, чтобы удалить элемент неопределенно-KO-подобным образом. Очевидно, что это не так хорошо, как правильный KO, но если вам действительно нужна производительность blazing (ish), это возможный обходной путь.

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

возможный обходной путь, в сочетании с использованием 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
  }
}

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

Я также заметил, что Knockout JS template engine работает медленнее в IE, Я заменил его подчеркиванием.js, работает намного быстрее.

при использовании IE попробуйте закрыть инструменты разработки.

открытие инструментов разработчика в IE значительно замедляет эту операцию. Я добавляю ~1000 элементов в массив. При открытии инструментов разработки это занимает около 10 секунд, и IE зависает, пока это происходит. Когда я закрываю инструменты разработки, операция выполняется мгновенно, и я не вижу замедления в IE.