Estou apenas começando com Knockout.js (sempre quis experimentar, mas agora finalmente tenho uma desculpa!) - No entanto, estou tendo alguns problemas de desempenho realmente ruins ao vincular uma tabela a um conjunto relativamente pequeno de dados (cerca de 400 linhas ou mais).
No meu modelo, tenho o seguinte código:
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!
}
};
O problema é que o for
loop acima leva cerca de 30 segundos ou mais com cerca de 400 linhas. No entanto, se eu mudar o código para:
this.loadData = function (data)
{
var testArray = []; //<-- Plain ol' Javascript array
for(var i = 0; i < data.length; i++)
{
testArray.push(new ResultRow(data[i]));
}
};
Em seguida, o for
loop é concluído em um piscar de olhos. Em outras palavras, o push
método do observableArray
objeto de Knockout é incrivelmente lento.
Aqui está o meu modelo:
<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>
Minhas perguntas:
- É esta a maneira certa de vincular meus dados (que vêm de um método AJAX) a uma coleção observável?
- Espero
push
estar fazendo um recálculo pesado toda vez que eu chamá-lo, como talvez reconstruir objetos DOM vinculados. Existe uma maneira de atrasar esse recálculo ou talvez enviar todos os meus itens de uma vez?
Posso adicionar mais código, se necessário, mas tenho certeza de que isso é o que é relevante. Na maior parte do tempo, estava apenas seguindo os tutoriais do Knockout do site.
ATUALIZAR:
Seguindo o conselho abaixo, atualizei meu código:
this.loadData = function (data)
{
var mappedData = $.map(data, function (item) { return new ResultRow(item) });
this.projects(mappedData);
};
No entanto, this.projects()
ainda leva cerca de 10 segundos para 400 linhas. Admito que não tenho certeza de quão rápido isso seria sem o Knockout (apenas adicionando linhas por meio do DOM), mas tenho a sensação de que seria muito mais rápido do que 10 segundos.
ATUALIZAÇÃO 2:
Por outro conselho abaixo, dei uma chance ao jQuery.tmpl (que é nativamente suportado pelo KnockOut), e este mecanismo de modelagem desenhará cerca de 400 linhas em pouco mais de 3 segundos. Esta parece ser a melhor abordagem, exceto uma solução que carregue dinamicamente mais dados conforme você rola.
fonte
valueHasMutated
faz isso. verifique a resposta se você tiver tempo.Respostas:
Como sugerido nos comentários.
Knockout tem seu próprio mecanismo de template nativo associado às ligações (foreach, with). Ele também oferece suporte a outros mecanismos de template, como jquery.tmpl. Leia aqui para mais detalhes. Não fiz nenhum benchmarking com motores diferentes, então não sei se vai ajudar. Lendo seu comentário anterior, no IE7 você pode ter dificuldade para obter o desempenho que deseja.
Como um aparte, KO suporta qualquer mecanismo de modelagem js, se alguém escreveu o adaptador para ele. Você pode querer experimentar outros, pois jquery tmpl será substituído pelo JsRender .
fonte
jquery.tmpl
então vou usar isso. Posso investigar outros motores, bem como escrever o meu próprio, se tiver algum tempo extra. Obrigado!data-bind
instruções em seu modelo jQuery ou está usando a sintaxe $ {code}?${code}
sintaxe e é muito mais rápido. Também tenho tentado fazer o Underscore.js funcionar, mas ainda não tive sorte (a<% .. %>
sintaxe interfere no ASP.NET) e ainda não parece haver suporte para JsRender.ResultRow
, ele não atualizará a IU (você terá que atualizar oprojects
observableArray que forçará uma nova renderização de sua tabela). $ {} pode definitivamente ser vantajoso se seus dados forem basicamente somente leituraConsulte: Knockout.js Performance Gotcha # 2 - Manipulando observableArrays
fonte
Use a paginação com KO além de usar $ .map.
Tive o mesmo problema com grandes conjuntos de dados de 1400 registros até usar paginação com knockout. Usar
$.map
para carregar os registros fez uma grande diferença, mas o tempo de renderização do DOM ainda era horrível. Em seguida, tentei usar a paginação, o que tornou a iluminação do conjunto de dados rápida e também mais amigável. Um tamanho de página de 50 tornou o conjunto de dados muito menos opressor e reduziu drasticamente o número de elementos DOM.É muito fácil de fazer com KO:
http://jsfiddle.net/rniemeyer/5Xr2X/
fonte
KnockoutJS tem ótimos tutoriais, especialmente aquele sobre como carregar e salvar dados
No caso deles, eles puxam dados usando o
getJSON()
que é extremamente rápido. Do exemplo deles: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); }); }
fonte
self.tasks(mappedTasks)
leva cerca de 10 segundos para ser executado (com 400 linhas). Eu sinto que isso ainda não é aceitável.+1
tanto por simplificar meu código quanto por aumentar drasticamente a velocidade. Talvez alguém tenha uma explicação mais detalhada de qual é o gargalo.Dê uma olhada no KoGrid . Ele gerencia de forma inteligente a renderização de linhas para que tenha mais desempenho.
Se você está tentando vincular 400 linhas a uma tabela usando uma
foreach
vinculação, terá problemas para enviar tudo isso por meio de KO para o DOM.KO faz algumas coisas muito interessantes usando a
foreach
vinculação, a maioria das quais são operações muito boas, mas elas começam a quebrar no perf conforme o tamanho do seu array aumenta.Já percorri o longo caminho escuro de tentar vincular grandes conjuntos de dados a tabelas / grades, e você acaba precisando dividir / paginar os dados localmente.
KoGrid faz tudo isso. Ele foi criado para renderizar apenas as linhas que o visualizador pode ver na página e, em seguida, virtualizar as outras linhas até que sejam necessárias. Acho que você descobrirá que seu desempenho em 400 itens é muito melhor do que o que está experimentando.
fonte
Uma solução para evitar travar o navegador ao renderizar um array muito grande é 'estrangular' o array de forma que apenas alguns elementos sejam adicionados de cada vez, com uma pausa entre eles. Esta é uma função que fará exatamente isso:
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; }
Dependendo do seu caso de uso, isso pode resultar em uma grande melhoria de UX, já que o usuário pode ver apenas o primeiro lote de linhas antes de ter que rolar.
fonte
Tirar vantagem de push () aceitar argumentos variáveis proporcionou o melhor desempenho no meu caso. 1.300 linhas estavam carregando por 5973ms (~ 6 segundos). Com essa otimização, o tempo de carregamento caiu para 914 ms (<1 seg.)
Isso é uma melhoria de 84,7%!
Mais informações em Empurrando itens para um 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); };
fonte
Tenho lidado com grandes volumes de dados que chegam para mim
valueHasMutated
funcionou como um encanto.Ver modelo:
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)
Depois de ligar
(4)
matriz, os dados serão carregados no observableArray necessário, que éthis.projects
automaticamente.se você tiver tempo, dê uma olhada nisto e apenas no caso de qualquer problema me avise
Truque aqui: Fazendo assim, se no caso de quaisquer dependências (computadas, inscrições etc) podem ser evitadas no nível push e podemos fazê-las executar de uma vez após a chamada
(4)
.fonte
push
, o problema é que mesmo uma única chamada para push causará longos tempos de renderização. Se uma matriz tem 1000 itens vinculados a umforeach
, empurrar um único item renderiza todo o foreach e você paga um alto custo de tempo de renderização.Uma possível solução alternativa, em combinação com o uso de jQuery.tmpl, é enviar itens por vez para a matriz observável de maneira assíncrona, usando 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 } }
Dessa forma, quando você adiciona apenas um único item por vez, o navegador / knockout.js pode demorar para manipular o DOM de acordo, sem que o navegador seja completamente bloqueado por vários segundos, para que o usuário possa rolar a lista simultaneamente.
fonte
Tenho experimentado desempenho e tenho duas contribuições que espero que sejam úteis.
Meus experimentos se concentram no tempo de manipulação do DOM. Portanto, antes de entrar nisso, definitivamente vale a pena seguir os pontos acima sobre como inserir uma matriz JS antes de criar uma matriz observável, etc.
Mas se o tempo de manipulação do DOM ainda estiver atrapalhando, isso pode ajudar:
1: Um padrão para envolver um spinner de carregamento em torno da renderização lenta e, em seguida, ocultá-lo usando afterRender
http://jsfiddle.net/HBYyL/1/
Isso não é realmente uma correção para o problema de desempenho, mas mostra que um atraso é provavelmente inevitável se você fizer um loop em milhares de itens e usar um padrão onde você pode garantir que um botão giratório de carregamento apareça antes da longa operação de KO e depois oculte depois. Portanto, melhora a UX, pelo menos.
Certifique-se de carregar um 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)
Ocultar o botão giratório:
<div data-bind="template: {afterRender: hide}">
que dispara:
hide = function() { $("#spinner").hide() }
2: Usando a ligação html como um hack
Lembrei-me de uma técnica antiga de quando estava trabalhando em um decodificador com o Opera, criando UI usando manipulação de DOM. Era terrivelmente lento, então a solução foi armazenar grandes pedaços de HTML como strings e carregar as strings configurando a propriedade innerHTML.
Algo semelhante pode ser alcançado usando a vinculação html e um computador que deriva o HTML para a tabela como um grande pedaço de texto e o aplica de uma só vez. Isso corrige o problema de desempenho, mas a grande desvantagem é que limita severamente o que você pode fazer com a vinculação dentro de cada linha da tabela.
Aqui está um violino que mostra essa abordagem, junto com uma função que pode ser chamada de dentro das linhas da tabela para excluir um item de uma forma vagamente KO. Obviamente, isso não é tão bom quanto um KO adequado, mas se você realmente precisa de um desempenho incrível (ish), esta é uma possível solução alternativa.
http://jsfiddle.net/9ZF3g/5/
fonte
Se estiver usando o IE, tente fechar as ferramentas de desenvolvimento.
Ter as ferramentas do desenvolvedor abertas no IE retarda significativamente essa operação. Estou adicionando ~ 1000 elementos a um array. Ao abrir as ferramentas de desenvolvimento, isso leva cerca de 10 segundos e o IE congela enquanto isso está acontecendo. Quando fecho as ferramentas de desenvolvimento, a operação é instantânea e não vejo lentidão no IE.
fonte
Também notei que o mecanismo de template Knockout js funciona mais devagar no IE, eu o substituí por underscore.js, funciona bem mais rápido.
fonte