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метод объекта 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>

Мои вопросы:

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

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

ОБНОВИТЬ:

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

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 ... Блек.
Майк Кристенсен,
1
ps, это кажется медленным, потому что вы добавляете 400 элементов в этот наблюдаемый массив по отдельности . Для каждого изменения наблюдаемого представление должно быть повторно отображено для всего, что зависит от этого массива. Для сложных шаблонов и большого количества элементов, которые нужно добавить, это очень накладные расходы, когда вы могли бы просто обновить массив сразу, установив его для другого экземпляра. По крайней мере, тогда рендеринг будет выполнен один раз.
Джефф Меркадо,
1
Я нашел способ более быстрый и аккуратный (ничего нестандартного). использование valueHasMutatedделает это. проверьте ответ, если есть время.
супер круто

Ответы:

16

Как было предложено в комментариях.

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

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

Madcapnmckay
источник
У меня улучшается производительность, 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
13

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

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

Это очень просто сделать с помощью КО:

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

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

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

Дополнительная информация в разделе Отправка элементов в 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);
};
митака
источник
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автоматически.

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

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

очень круто
источник
1
Проблема не в большом количестве вызовов push, проблема в том, что даже один вызов 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
  }
}

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

грызть
источник
2
Это заставит N обновлений DOM, что приведет к общему времени рендеринга, которое намного больше, чем выполнение всего сразу.
Fredrik C
Конечно, это правильно. Дело, однако, в том, что комбинация большого числа N и помещения элемента в массив проектов, вызывающего значительное количество других обновлений или вычислений DOM, может привести к зависанию браузера и предложению убить вкладку. Имея тайм-аут, либо на элемент, либо на 10, 100 или какое-либо другое количество элементов, браузер все равно будет реагировать.
gnab 07
2
Я бы сказал, что это неправильный подход в общем случае, когда полное обновление не замораживает браузер, но его можно использовать, когда все остальные не работают. Для меня это звучит как плохо написанное приложение, в котором проблемы с производительностью должны быть решены, а не просто заставить его не зависать.
Fredrik C
1
Конечно, в общем случае это неправильный подход, в этом с вами никто не согласится. Это хитрость и проверка концепции предотвращения зависания браузера, если вам нужно выполнять множество операций DOM. Мне это было нужно пару лет назад, когда я перечислял несколько больших HTML-таблиц с несколькими привязками на ячейку, в результате чего оценивались тысячи привязок, каждая из которых влияла на состояние DOM. Функциональность была необходима временно, для проверки правильности повторной реализации настольного приложения на основе Excel в качестве веб-приложения. Тогда это решение сработало идеально.
gnab 07
Комментарий в основном предназначался для прочтения другими, чтобы не предполагать, что это предпочтительный способ. Я предположил, что вы знали, что делаете.
Fredrik C
1

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

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

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.
Марчелло