Knockout.js incrivelmente lento em conjuntos de dados semi-grandes

86

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 forloop 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 forloop é concluído em um piscar de olhos. Em outras palavras, o pushmétodo do observableArrayobjeto 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:

  1. É esta a maneira certa de vincular meus dados (que vêm de um método AJAX) a uma coleção observável?
  2. Espero pushestar 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.

Mike Christensen
fonte
1
Você está usando a ligação foreach nocaute ou a ligação modelo com foreach. Só estou me perguntando se usar o template e incluir jquery tmpl em vez do mecanismo de template nativo pode fazer a diferença.
madcapnmckay
1
@MikeChristensen - 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.
madcapnmckay
2
Considerando que acabamos de comprar o IE7 há alguns meses, acho que o IE9 será lançado no verão de 2019. Oh, estamos todos no WinXP também ... Blech.
Mike Christensen,
1
ps, O motivo pelo qual parece lento é que você está adicionando 400 itens a essa matriz observável individualmente . Para cada alteração no observável, a exibição deve ser renderizada novamente para qualquer coisa que dependa dessa matriz. Para modelos complexos e muitos itens a serem adicionados, isso representa uma grande sobrecarga quando você poderia apenas atualizar o array de uma só vez configurando-o para uma instância diferente. Pelo menos então, a re-renderização seria feita uma vez.
Jeff Mercado,
1
Eu encontrei uma maneira mais rápida e simples (nada fora da caixa). usando valueHasMutatedfaz isso. verifique a resposta se você tiver tempo.
super legal

Respostas:

16

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 .

madcapnmckay
fonte
Estou obtendo um desempenho muito melhor com, jquery.tmplentão vou usar isso. Posso investigar outros motores, bem como escrever o meu próprio, se tiver algum tempo extra. Obrigado!
Mike Christensen,
1
@MikeChristensen - você ainda está usando data-bindinstruções em seu modelo jQuery ou está usando a sintaxe $ {code}?
ericb
@ericb - Com o novo código, estou usando ${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.
Mike Christensen de
1
@MikeChristensen - ok, então isso faz sentido. O mecanismo de template nativo do KO não é necessariamente ineficiente. Quando você usa a sintaxe $ {code}, você não obtém qualquer vinculação de dados nesses elementos (o que melhora o desempenho). Portanto, se você alterar uma propriedade de a ResultRow, ele não atualizará a IU (você terá que atualizar o projectsobservableArray que forçará uma nova renderização de sua tabela). $ {} pode definitivamente ser vantajoso se seus dados forem basicamente somente leitura
ericb
4
Necromancia! jquery.tmpl não está mais em desenvolvimento
Alex Larzelere
50

Consulte: Knockout.js Performance Gotcha # 2 - Manipulando observableArrays

Um padrão melhor é obter uma referência para nossa matriz subjacente, enviar por push e chamar .valueHasMutated (). Agora, nossos assinantes receberão apenas uma notificação indicando que a matriz foi alterada.

Jim G.
fonte
13

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 $.mappara 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/

Tim Santeford
fonte
11

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);
    });    
}
Deltree
fonte
1
Definitivamente, uma grande melhoria, mas self.tasks(mappedTasks)leva cerca de 10 segundos para ser executado (com 400 linhas). Eu sinto que isso ainda não é aceitável.
Mike Christensen
Concordo que 10 segundos não é aceitável. Usando knockoutjs, não tenho certeza do que é melhor do que um mapa, então vou adicionar esta pergunta como favorito e esperar por uma resposta melhor.
deltree
1
Está bem. A resposta definitivamente merece +1tanto por simplificar meu código quanto por aumentar drasticamente a velocidade. Talvez alguém tenha uma explicação mais detalhada de qual é o gargalo.
Mike Christensen
9

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 foreachvinculação, terá problemas para enviar tudo isso por meio de KO para o DOM.

KO faz algumas coisas muito interessantes usando a foreachvinculaçã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.

Ericb
fonte
1
Isso parece estar completamente quebrado no IE7 (nenhum dos exemplos funciona), caso contrário, seria ótimo!
Mike Christensen,
Fico feliz em dar uma olhada nisso - KoGrid ainda está em desenvolvimento ativo. No entanto, isso pelo menos responde à sua pergunta sobre o desempenho?
ericb
1
Sim! Isso confirma minha suspeita original de que o mecanismo de template KO padrão é bastante lento. Se você precisa de alguém para cobaia KoGrid para você, eu ficaria feliz. Parece exatamente o que precisamos!
Mike Christensen,
Droga. Isso parece muito bom! Infelizmente, mais de 50% dos usuários do meu aplicativo usam o IE7!
Jim G.
Interessante, hoje em dia temos que suportar relutantemente o IE11. As coisas melhoraram nos últimos 7 anos.
MrBoJangles 01 de
5

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.

teh_senaus
fonte
Eu gosto dessa solução, mas em vez de setTimeout a cada iteração, recomendo apenas executar setTimout a cada 20 ou mais iterações porque sempre leva muito tempo para carregar. Vejo que você está fazendo isso com o +20, mas não era óbvio para mim à primeira vista.
charlierlee
5

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);
};
mitaka
fonte
4

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.projectsautomaticamente.

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).

muito legal
fonte
1
O problema não são muitas chamadas para 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.
Ligeiro dia
1

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.

gnab
fonte
2
Isso forçará um número N de atualizações de DOM, o que resultará em um tempo de renderização total muito mais longo do que fazer tudo de uma vez.
Fredrik C
Claro que isso está correto. O ponto é, no entanto, que a combinação de N sendo um grande número e empurrando um item para a matriz de projetos, acionando uma quantidade significativa de outras atualizações ou cálculos DOM, pode fazer com que o navegador congele e ofereça a você o encerramento da guia. Por ter um tempo limite, por item ou por 10, 100 ou algum outro número de itens, o navegador ainda responderá.
gnab
2
Eu diria que essa é a abordagem errada no caso geral em que a atualização total não congelaria o navegador, mas é algo para usar quando todas as outras falhas. Para mim, parece um aplicativo mal escrito, onde os problemas de desempenho devem ser resolvidos em vez de apenas impedir que congele.
Fredrik C
1
Claro que é a abordagem errada no caso geral, ninguém discordaria de você nisso. Este é um hack e uma prova de conceito para evitar o travamento do navegador se você precisar fazer muitas operações DOM. Eu precisei disso alguns anos atrás, ao listar várias tabelas HTML grandes com várias associações por célula, resultando em milhares de associações sendo avaliadas, cada uma afetando o estado do DOM. A funcionalidade foi necessária temporariamente, para verificar a exatidão da reimplementação de um aplicativo de desktop baseado em Excel como um aplicativo da web. Então essa solução funcionou perfeitamente.
gnab
O comentário era principalmente para outras pessoas lerem, para não presumir que essa era a forma preferida. Presumi que você sabia o que estava fazendo.
Fredrik C
1

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/

sifriday
fonte
1

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.

Jon List
fonte
0

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.

Marcello
fonte
Como você fez isso, por favor?
Stu Harper
@StuHarper Importei a biblioteca de sublinhado e, em seguida, em main.js, segui as etapas descritas na seção de integração de sublinhado de knockoutjs.com/documentation/template-binding.html
Marcello
Com qual versão do IE essa melhoria ocorreu?
bkwdesign
@bkwdesign Eu estava usando o IE 10, 11.
Marcello