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

Я только начинаю с 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 объекта Knockout 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>

Мои вопросы:

  • Это правильный способ привязать мои данные (которые исходят от метода AJAX) к наблюдаемой коллекции?
  • Я ожидаю, что push будет выполнять тяжелый повторный подсчет каждый раз, когда я его назову, например, возможно перестроить связанные объекты DOM. Есть ли способ либо отсрочить этот recalc, либо, возможно, нажать на все мои предметы одновременно?

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

UPDATE:

В приведенном ниже совете я обновил свой код:

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

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

ОБНОВЛЕНИЕ 2:

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

Ответ 1

Как указано в комментариях.

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

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

Ответ 2

Смотрите: Knockout.js Производительность Gotcha # 2 - Манипулирование наблюдаемыми массивами

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

Ответ 3

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

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

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

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

Ответ 4

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

Ответ 5

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

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

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

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

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

Ответ 6

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

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

Ответ 7

Воспользовавшись аргументами accept() accepting(), которые дают переменные, в моем случае была лучшая производительность. 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);
};

Ответ 8

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

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

Трюк здесь:. Таким образом, если в случае любых зависимостей (вычисленных, подписчиков и т.д.) можно избежать на уровне push, и мы можем заставить их выполнить за один проход после вызова (4).

Ответ 9

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

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

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


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

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

На самом деле это не проблема для проблемы с производительностью, но показывает, что задержка, вероятно, неизбежна, если вы зацикливаете более тысячи элементов и используете шаблон, в котором вы можете убедиться, что у вас есть счетчик загрузки перед длинной операцией KO, затем спрячьте его потом. Таким образом, он улучшает 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 для таблицы как большой фрагмент текста, а затем применяет его за один раз. Это устраняет проблему производительности, но огромный недостаток заключается в том, что он сильно ограничивает то, что вы можете сделать с привязкой внутри каждой строки таблицы.

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

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

Ответ 10

Возможная совместная работа в сочетании с использованием 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 соответственно, без блокировки браузера в течение нескольких секунд, чтобы пользователь мог прокручивать список одновременно.

Ответ 11

Я также заметил, что механизм IE для нокаута js работает медленнее в IE, я заменил его на underscore.js, работает быстрее.

Ответ 12

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

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