javascript performance knockout.js

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



performance (12)

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

Más información en Empujar 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); };

Estoy empezando con Knockout.js (siempre quise probarlo, ¡pero ahora finalmente tengo una excusa!) - Sin embargo, me estoy encontrando con algunos problemas de rendimiento realmente malos al vincular una tabla a un conjunto relativamente pequeño de 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 ciclo de arriba toma 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 bucle for completa en un abrir y cerrar de ojos. En otras palabras, el método push del objeto observableArray Array 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 manera correcta de vincular mis datos (que provienen de un método AJAX) a una colección observable?
  2. Supongo que push está haciendo una gran recalculación cada vez que lo llamo, como por ejemplo, la reconstrucción de objetos DOM encuadernados. ¿Hay alguna manera de retrasar este recalc, o quizás empujar 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:

Según el consejo 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() aún tarda unos 10 segundos para 400 filas. Admito que no estoy seguro de lo rápido que sería sin Knockout (simplemente 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 nativamente con KnockOut), y este motor de plantillas dibujará alrededor de 400 filas en poco más de 3 segundos. Este parece ser el mejor enfoque, a falta de una solución que cargue dinámicamente más datos a medida que se desplaza.


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. Lee here para más detalles. No he hecho ningún benchmarking con diferentes motores, así que no sé si será útil. 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 eso. Es posible que desee probar otros por ahí ya que jquery tmpl debe ser reemplazado por JsRender .


Dale un vistazo a KoGrid . Gestiona inteligentemente su representación de filas para que sea más eficiente.

Si intentas enlazar 400 filas a una tabla con un enlace foreach , vas a tener problemas para empujar esa cantidad a través de KO al DOM.

KO hace algunas cosas muy interesantes utilizando el enlace foreach , la mayoría de las cuales son muy buenas operaciones, pero comienzan a descomponerse a medida que aumenta el tamaño de su matriz.

He recorrido el largo y oscuro camino de intentar enlazar grandes conjuntos de datos a tablas / cuadrículas, y terminas necesitando separar / paginar los datos localmente.

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


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

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

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

1: un patrón para envolver un girador 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 aparece un indicador de carga antes de la operación de KO prolongada, luego ocultar después Por lo tanto, mejora la experiencia de usuario, al menos.

Asegúrate de que puedes cargar un girador:

// 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 el spinner:

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

que desencadena:

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

2: Usar el enlace html como un hack

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

Se puede lograr algo similar utilizando el enlace html y un cálculo que deriva el HTML de la tabla como una gran porción de texto, y luego lo aplica de una vez. Esto soluciona el problema de rendimiento, pero la desventaja masiva es que limita severamente 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 correcto, pero si realmente necesitas un rendimiento impresionante (ish), esta es una posible solución.

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


He estado lidiando con enormes volúmenes de datos que me han valueHasMutated a la valueHasMutated funcionaba como un hechizo.

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 (4) datos de la matriz se cargarán en el this.projects observable requerido que es this.projects automáticamente.

si tiene tiempo, eche un vistazo a esto y, por si acaso, algún problema, hágamelo saber.

Truco aquí: haciendo esto, si en caso de cualquier dependencia (computada, suscrita, etc.) se puede evitar en el nivel de inserción y podemos hacer que se ejecuten de una vez después de llamar (4) .


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

En su caso, getJSON() datos utilizando getJSON() que es 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); }); }



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


Una posible solución alternativa, en combinación con el uso de jQuery.tmpl, consiste en insertar elementos a la vez en la matriz observable de forma asíncrona, utilizando 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 forma, cuando solo agrega un elemento a la vez, el navegador / knockout.js puede tomar su tiempo para manipular el DOM en consecuencia, sin que el navegador se bloquee por completo durante varios segundos, de modo que el usuario pueda desplazarse por la lista simultáneamente.


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á exactamente 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 dar como resultado una mejora masiva de UX, ya que el usuario solo podría ver el primer lote de filas antes de tener que desplazarse.


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 demora unos 10 segundos y IE se congela mientras está sucediendo. Cuando cierro las herramientas de desarrollo, la operación es instantánea y no veo que disminuya la velocidad en IE.


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

Tuve el mismo problema con un gran conjunto de datos de 1400 registros hasta que usé paginación con knockout. Usar $.map para cargar los registros sí marcó una gran diferencia, pero el tiempo de renderización de DOM aún era horrible. Luego traté de usar la paginación y eso hizo que mi conjunto de datos se iluminara tan rápido como mejor para el usuario. Un tamaño de página de 50 hace que el conjunto de datos sea mucho menos abrumador y reduce drásticamente la cantidad de elementos DOM.

Es muy fácil de hacer con KO:

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