Knockout.js increíblemente lento en conjuntos de datos semi-grandes

86

Recién estoy comenzando con Knockout.js (siempre quise probarlo, ¡pero ahora finalmente tengo una excusa!). Sin embargo, me encuentro con algunos problemas de rendimiento realmente malos cuando vincula una tabla a un conjunto relativamente pequeño datos (alrededor de 400 filas más o menos).

En mi modelo, tengo el siguiente 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!
   }
};

El problema es que el forbucle anterior tarda unos 30 segundos más o menos con alrededor de 400 filas. Sin embargo, si cambio el código a:

this.loadData = function (data)
{
   var testArray = []; //<-- Plain ol' Javascript array
   for(var i = 0; i < data.length; i++)
   {
      testArray.push(new ResultRow(data[i]));
   }
};

Luego, el forbucle se completa en un abrir y cerrar de ojos. En otras palabras, el pushmétodo del observableArrayobjeto de Knockout es increíblemente lento.

Aquí está mi plantilla:

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

Mis preguntas:

  1. ¿Es esta la forma correcta de vincular mis datos (que provienen de un método AJAX) a una colección observable?
  2. Espero que pushesté haciendo una recalculación pesada cada vez que lo llamo, como quizás reconstruir objetos DOM enlazados. ¿Hay alguna manera de retrasar este recalc o tal vez introducir todos mis elementos a la vez?

Puedo agregar más código si es necesario, pero estoy bastante seguro de que esto es lo relevante. En su mayor parte, solo estaba siguiendo los tutoriales de Knockout del sitio.

ACTUALIZAR:

Siguiendo los consejos a continuación, he actualizado mi código:

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

Sin embargo, this.projects()todavía se necesitan unos 10 segundos para 400 filas. Admito que no estoy seguro de qué tan rápido sería esto sin Knockout (solo agregando filas a través del DOM), pero tengo la sensación de que sería mucho más rápido que 10 segundos.

ACTUALIZACIÓN 2:

Según otros consejos a continuación, le di una oportunidad a jQuery.tmpl (que es compatible de forma nativa con KnockOut), y este motor de plantillas dibujará alrededor de 400 filas en poco más de 3 segundos. Este parece el mejor enfoque, salvo una solución que cargue dinámicamente más datos a medida que se desplaza.

Mike Christensen
fuente
1
¿Está utilizando una encuadernación de nocaut para cada uno o la encuadernación de plantilla con foreach? Me pregunto si usar una plantilla e incluir jquery tmpl en lugar del motor de plantillas nativo puede marcar la diferencia.
madcapnmckay
1
@MikeChristensen: Knockout tiene su propio motor de plantilla nativo asociado con los enlaces (foreach, with). También es compatible con otros motores de plantillas, a saber, jquery.tmpl. Lea aquí para obtener más detalles. No he hecho ninguna evaluación comparativa con diferentes motores, así que no sé si ayudará. Al leer su comentario anterior, en IE7 puede tener dificultades para obtener el rendimiento que busca.
madcapnmckay
2
Teniendo en cuenta que tenemos IE7 hace unos meses, creo que IE9 se lanzará alrededor del verano de 2019. Oh, todos estamos en WinXP también ... Blech.
Mike Christensen
1
ps, la razón por la que parece lento es que está agregando 400 elementos a esa matriz observable individualmente . Para cada cambio en lo observable, la vista debe volver a representarse para cualquier cosa que dependa de esa matriz. Para plantillas complejas y muchos elementos para agregar, eso es una gran sobrecarga cuando podría haber actualizado la matriz de una vez configurándola en una instancia diferente. Al menos entonces, la rendición se haría una vez.
Jeff Mercado
1
Encontré una forma más rápida y ordenada (nada fuera de la caja). usar lo valueHasMutatedhace. compruebe la respuesta si tiene tiempo.
super cool

Respuestas:

16

Como se sugiere en los comentarios.

Knockout tiene su propio motor de plantilla nativo asociado con los enlaces (foreach, with). También es compatible con otros motores de plantillas, a saber, jquery.tmpl. Lea aquí para obtener más detalles. No he hecho ninguna evaluación comparativa con diferentes motores, así que no sé si ayudará. Al leer su comentario anterior, en IE7 puede tener dificultades para obtener el rendimiento que busca.

Por otro lado, KO admite cualquier motor de plantillas js, si alguien ha escrito el adaptador para él. Es posible que desee probar otros, ya que jquery tmpl será reemplazado por JsRender .

madcapnmckay
fuente
Estoy obteniendo un rendimiento mucho mejor, jquery.tmplasí que lo usaré . Podría investigar otros motores y escribir el mío propio si tengo algo de tiempo extra. ¡Gracias!
Mike Christensen
1
@MikeChristensen: ¿todavía usa data-binddeclaraciones en su plantilla jQuery o está usando la sintaxis $ {code}?
ericb
@ericb: con el nuevo código, estoy usando ${code}sintaxis y es mucho más rápido. También he intentado que Underscore.js funcione, pero aún no he tenido suerte (la <% .. %>sintaxis interfiere con ASP.NET) y todavía no parece haber compatibilidad con JsRender.
Mike Christensen
1
@MikeChristensen - ok, entonces esto tiene sentido. El motor de plantillas nativo de KO no es necesariamente tan ineficiente. Cuando usa la sintaxis $ {code}, no obtiene ningún enlace de datos en esos elementos (lo que mejora el rendimiento). Por lo tanto, si cambia una propiedad de a ResultRow, no actualizará la interfaz de usuario (tendrá que actualizar el projectsobservableArray que forzará una nueva representación de su tabla). $ {} definitivamente puede ser ventajoso si sus datos son prácticamente de solo lectura
ericb
4
¡Nigromancia! jquery.tmpl ya no está en desarrollo
Alex Larzelere
13

Use la paginación con KO además de usar $ .map.

Tuve el mismo problema con grandes conjuntos de datos de 1400 registros hasta que usé la paginación con knockout. Usar $.mappara cargar los registros marcó una gran diferencia, pero el tiempo de renderizado del DOM seguía siendo horrible. Luego intenté usar la paginación y eso hizo que la iluminación de mi conjunto de datos fuera rápida y más fácil de usar. Un tamaño de página de 50 hizo que el conjunto de datos fuera mucho menos abrumador y redujo drásticamente la cantidad de elementos DOM.

Es muy fácil de hacer con KO:

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

Tim Santeford
fuente
11

KnockoutJS tiene excelentes tutoriales, particularmente el de cargar y guardar datos

En su caso, extraen datos con un uso getJSON()extremadamente rápido. De su ejemplo:

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
fuente
1
Definitivamente una gran mejora, pero self.tasks(mappedTasks)tarda unos 10 segundos en ejecutarse (con 400 filas). Siento que esto todavía no es aceptable.
Mike Christensen
Estoy de acuerdo en que 10 segundos no son aceptables. Usando knockoutjs, no estoy seguro de qué es mejor que un mapa, así que marcaré esta pregunta como favorita y esperaré una mejor respuesta.
Deltree
1
Okay. La respuesta definitivamente merece un +1tanto para simplificar mi código como para aumentar la velocidad drásticamente. Quizás alguien tenga una explicación más detallada de qué es el cuello de botella.
Mike Christensen
9

Dar KoGrid un vistazo. Gestiona de forma inteligente el renderizado de filas para que sea más eficaz.

Si está tratando de vincular 400 filas a una tabla usando una foreachvinculación, tendrá problemas para empujar eso a través de KO al DOM.

KO hace algunas cosas muy interesantes usando el foreachenlace, la mayoría de las cuales son muy buenas operaciones, pero comienzan a fallar en el rendimiento a medida que crece el tamaño de su matriz.

He estado en el largo y oscuro camino de tratar de vincular grandes conjuntos de datos a tablas / cuadrículas, y terminas necesitando dividir / paginar los datos localmente.

KoGrid hace todo esto. Se ha creado para representar solo las filas que el espectador puede ver en la página y luego virtualizar las otras filas hasta que sean necesarias. Creo que encontrará que su rendimiento en 400 elementos es mucho mejor de lo que está experimentando.

ericb
fuente
1
Esto parece estar completamente roto en IE7 (ninguna de las muestras funciona), de lo contrario, ¡sería genial!
Mike Christensen
Me alegro de analizarlo: KoGrid todavía está en desarrollo activo. Sin embargo, ¿esto al menos responde a su pregunta sobre el rendimiento?
Ericb
1
¡Sip! Confirma mi sospecha original de que el motor de plantilla de KO predeterminado es bastante lento. Si necesitas a alguien que conecte KoGrid por ti, estaré feliz de hacerlo. ¡Suena exactamente como lo que necesitamos!
Mike Christensen
Maldito. ¡Esto se ve muy bien! Desafortunadamente, más del 50% de los usuarios de mi aplicación usan IE7.
Jim G.
Interesante, hoy en día tenemos que admitir a regañadientes IE11. Las cosas han mejorado en los últimos siete años.
MrBoJangles
5

Una solución para evitar bloquear el navegador cuando se renderiza una matriz muy grande es "estrangular" la matriz de modo que solo se agreguen unos pocos elementos a la vez, con un descanso entre ellos. Aquí hay una función que hará precisamente eso:

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

Dependiendo de su caso de uso, esto podría resultar en una mejora masiva de UX, ya que es posible que el usuario solo vea el primer lote de filas antes de tener que desplazarse.

teh_senaus
fuente
Me gusta esta solución, pero en lugar de setTimeout en cada iteración, recomiendo ejecutar solo setTimout cada 20 o más iteraciones porque cada vez también tarda demasiado en cargarse. Veo que estás haciendo eso con el +20, pero no fue obvio para mí a primera vista.
charlierlee
5

Aprovechar la aceptación de push () de argumentos variables dio el mejor rendimiento en mi caso. Se cargaron 1300 filas durante 5973 ms (~ 6 segundos). Con esta optimización, el tiempo de carga se redujo a 914 ms (<1 seg.) ¡
Eso es una mejora del 84,7%!

Más información en Cómo enviar elementos a una matriz observable

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
fuente
4

He estado lidiando con volúmenes tan grandes de datos que me han valueHasMutatedfuncionado de maravilla.

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) 

Después de llamar, los (4)datos de la matriz se cargarán en la matriz observable requerida, que es this.projectsautomáticamente.

Si tienes tiempo, mira esto y, en caso de que tengas algún problema, avísame.

Truco aquí: Al hacer esto, si en el caso de cualquier dependencia (calculada, suscripción, etc.) se puede evitar a nivel de inserción y podemos hacer que se ejecuten de una vez después de la llamada (4).

Super guay
fuente
1
El problema no son demasiadas llamadas a push, el problema es que incluso una sola llamada a empujar provocará tiempos de procesamiento prolongados. Si una matriz tiene 1000 elementos vinculados a a foreach, al presionar un solo elemento se vuelve a reproducir todo el foreach y se paga un gran costo de tiempo de procesamiento.
Ligero
1

Una posible solución, en combinación con el uso de jQuery.tmpl, es enviar elementos a la vez a la matriz observable de manera asincrónica, 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
  }
}

De esta manera, cuando solo agrega un solo elemento a la vez, el navegador / knockout.js puede tomarse su tiempo para manipular el DOM en consecuencia, sin que el navegador esté completamente bloqueado durante varios segundos, de modo que el usuario pueda desplazarse por la lista simultáneamente.

gnab
fuente
2
Esto forzará N número de actualizaciones DOM que resultarán en un tiempo de renderizado total que es mucho más largo que hacer todo a la vez.
Fredrik C
Eso es, por supuesto, correcto. El punto es, sin embargo, que la combinación de N es un número grande y empujar un elemento en la matriz de proyectos que desencadena una cantidad significativa de otras actualizaciones o cálculos DOM, puede hacer que el navegador se congele y le ofrezca eliminar la pestaña. Al tener un tiempo de espera, ya sea por elemento o por 10, 100 o alguna otra cantidad de elementos, el navegador seguirá respondiendo.
gnab
2
Diría que este es el enfoque incorrecto en el caso general en el que la actualización total no congelaría el navegador, pero es algo para usar cuando todos los demás fallan. Para mí, suena como una aplicación mal escrita donde los problemas de rendimiento deben resolverse en lugar de simplemente hacer que no se congele.
Fredrik C
1
Por supuesto, es el enfoque equivocado en el caso general, nadie estaría en desacuerdo contigo en eso. Este es un truco y una prueba de concepto para evitar que el navegador se congele si necesita realizar muchas operaciones DOM. Lo necesitaba hace un par de años cuando enumeraba varias tablas HTML grandes con varios enlaces por celda, lo que resultó en la evaluación de miles de enlaces, cada uno de los cuales afectaba el estado del DOM. La funcionalidad se necesitaba temporalmente para verificar la corrección de la reimplementación de una aplicación de escritorio basada en Excel como aplicación web. Entonces esta solución funcionó perfectamente.
gnab
El comentario fue principalmente para que otros lo leyeran para no asumir que esta era la forma preferida. Supuse que sabías lo que estabas haciendo.
Fredrik C
1

He estado experimentando con el rendimiento y tengo dos contribuciones que espero sean útiles.

Mis experimentos se centran en el tiempo de manipulación del DOM. Entonces, antes de entrar en esto, definitivamente vale la pena seguir los puntos anteriores sobre cómo insertar una matriz JS antes de crear una matriz observable, etc.

Pero si el tiempo de manipulación del DOM todavía se interpone en su camino, entonces esto podría ayudar:


1: Un patrón para envolver una ruleta de carga alrededor del renderizado lento, luego ocultarlo usando afterRender

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

Esto no es realmente una solución para el problema de rendimiento, pero muestra que un retraso es probablemente inevitable si recorre miles de elementos y utiliza un patrón en el que puede asegurarse de que aparezca un control giratorio de carga antes de la larga operación de KO y luego se esconda luego. Así que mejora la UX, al menos.

Asegúrate de poder cargar una ruleta:

// 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 la ruleta:

<div data-bind="template: {afterRender: hide}">

que desencadena:

hide = function() {
    $("#spinner").hide()
}

2: Usar el enlace html como truco

Recordé una vieja técnica de cuando estaba trabajando en un decodificador con Opera, construyendo UI usando manipulación DOM. Era terriblemente lento, por lo que la solución fue almacenar grandes trozos de HTML como cadenas y cargar las cadenas estableciendo la propiedad innerHTML.

Algo similar se puede lograr utilizando el enlace html y un cálculo que deriva el HTML de la tabla como una gran parte de texto y luego lo aplica de una vez. Esto soluciona el problema de rendimiento, pero la gran desventaja es que limita en gran medida lo que puede hacer con el enlace dentro de cada fila de la tabla.

Aquí hay un violín que muestra este enfoque, junto con una función que se puede llamar desde dentro de las filas de la tabla para eliminar un elemento de una manera vagamente similar a KO. Obviamente, esto no es tan bueno como el KO adecuado, pero si realmente necesita un rendimiento increíble (ish), esta es una posible solución.

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

viernes
fuente
1

Si usa IE, intente cerrar las herramientas de desarrollo.

Tener las herramientas de desarrollador abiertas en IE ralentiza significativamente esta operación. Estoy agregando ~ 1000 elementos a una matriz. Cuando se abren las herramientas de desarrollo, esto toma alrededor de 10 segundos e IE se congela mientras sucede. Cuando cierro las herramientas de desarrollo, la operación es instantánea y no veo desaceleración en IE.

Jon List
fuente
0

También noté que el motor de plantilla Knockout js funciona más lento en IE, lo reemplacé con underscore.js, funciona mucho más rápido.

Marcello
fuente
¿Cómo hiciste esto por favor?
Stu Harper
@StuHarper Importé la biblioteca de subrayado y luego en main.js seguí los pasos descritos en la sección de integración de subrayado de knockoutjs.com/documentation/template-binding.html
Marcello
¿Con qué versión de IE se produjo esta mejora?
bkwdesign
@bkwdesign Estaba usando IE 10, 11.
Marcello