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 for
bucle 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 for
bucle se completa en un abrir y cerrar de ojos. En otras palabras, el push
método del observableArray
objeto 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:
- ¿Es esta la forma correcta de vincular mis datos (que provienen de un método AJAX) a una colección observable?
- Espero que
push
esté 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.
fuente
valueHasMutated
hace. compruebe la respuesta si tiene tiempo.Respuestas:
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 .
fuente
jquery.tmpl
así que lo usaré . Podría investigar otros motores y escribir el mío propio si tengo algo de tiempo extra. ¡Gracias!data-bind
declaraciones en su plantilla jQuery o está usando la sintaxis $ {code}?${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.ResultRow
, no actualizará la interfaz de usuario (tendrá que actualizar elprojects
observableArray que forzará una nueva representación de su tabla). $ {} definitivamente puede ser ventajoso si sus datos son prácticamente de solo lecturaConsulte: Knockout.js Performance Gotcha # 2 - Manipulación de matrices observables
fuente
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
$.map
para 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/
fuente
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); }); }
fuente
self.tasks(mappedTasks)
tarda unos 10 segundos en ejecutarse (con 400 filas). Siento que esto todavía no es aceptable.+1
tanto 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.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
foreach
vinculación, tendrá problemas para empujar eso a través de KO al DOM.KO hace algunas cosas muy interesantes usando el
foreach
enlace, 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.
fuente
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.
fuente
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); };
fuente
He estado lidiando con volúmenes tan grandes de datos que me han
valueHasMutated
funcionado 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 esthis.projects
automá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)
.fuente
push
, el problema es que incluso una sola llamada a empujar provocará tiempos de procesamiento prolongados. Si una matriz tiene 1000 elementos vinculados a aforeach
, al presionar un solo elemento se vuelve a reproducir todo el foreach y se paga un gran costo de tiempo de procesamiento.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.
fuente
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/
fuente
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.
fuente
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.
fuente