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.