Ingeniería de redes de datos rápidas: lecciones de la optimización de Ignite UI para registros de datos de 1M+
El rendimiento en la parrilla no se reduce solo a la velocidad. Se trata de consistencia bajo una carga de datos elevada. Cuando una red se congela durante las operaciones de datos, se siente lento e poco fiable. En los flujos de trabajo de toma de decisiones en tiempo real, esa falta de fiabilidad se convierte en una carga.
Para los desarrolladores que construyen sistemas de finanzas, banca, ERP y otros que requieren muchos datos, la red de datos suele ser el principal límite de rendimiento: el "bucle caliente" donde la ordenación y filtrado entre grandes conjuntos de datos compiten por el tiempo del hilo principal. En estos casos, pequeñas ineficiencias se vuelven rápidamente visibles para el usuario y rompen la interacción.
Pero encontramos una solución. Esta publicación mostrará cómo optimizamos la ordenación y el filtrado para mantener Ignite UI rápido en 1M+ filas entre frameworks (Angular, React, Blazor, Web Components). Nos centraremos en la ordenación y filtrado concreto de la cuadrícula de datos, los cambios que funcionaron y los que no.
Veamos qué hemos hecho.
La realidad antes de la optimización: Donde las cosas empezaron a fallar
Cada problema de rendimiento comienza igual: una arquitectura que era razonable a una escala se convierte en un cuello de botella en otra. Funciones como la ordenación, agrupación y filtrado de Ignite UI no fueron una excepción.
Clasificación: El coste oculto de la resolución de valores
La tubería de ordenación principal funcionaba recursivamente, procesando cada expresión de ordenación en secuencia. Para la ordenación de varias columnas, tras ordenar por la expresión primaria, agrupaba los registros de igual valor y ordenaba recursivamente cada grupo por la siguiente expresión. Limpio, correcto y completamente razonable para conjuntos de datos pequeños.
El problema era el resolvedor de valores.
Debido a que la cuadrícula soporta múltiples tipos de datos columnas – partes de fecha de los objetos Fecha, partes de tiempo de objetos Fecha, cadenas, números, objetos jerárquicos clave-valor – cada comparación de valores requería resolver el valor del campo en tiempo de ejecución. El resolvedor de valores gestionaba el recorrido de caminos, análisis de fechas, normalización temporal, análisis numérico, todo en cada comparación. Se llamaba dos veces por operación de comparación, una vez para cada lado:
compare(recordA, recordB):
valA = resolveValue(recordA, field) // path traversal + date parsing + type coercion
valB = resolveValue(recordB, field) // same cost, every single comparison
return compareValues(valA, valB)
Para un ordenamiento estándar de comparación, eso esComparaciones, con el resolver llamado dos veces por comparación. Con 100.000 filas: 3,4 millones de llamadas a resolver por columna ordenada. En 1 millón de filas: 40 millones de llamadas a resolvers. Cada uno hace resolución de rutas en tiempo de ejecución y posible análisis de fechas, sin caché entre llamadas.
Pero el comparador de clasificación no fue el único lugar donde se invocó el resolvedor de valores. Para la ordenación de varias columnas, tras ordenar por expresión i, el algoritmo necesitaba encontrar grupos de valores iguales antes de ordenar por expresión i+1. Esta detección de grupos iteraba sobre cada registro, llamando al resolver una vez por registro, una opción adicionalPasa por encima de la clasificación.
Así, para una ordenación de dos columnas sobre 1M filas, se invocó el resolución de valores del orden de + A veces solo para la primera expresión – antes de que la segunda se tocara.
- A 10.000 filas: imperceptible.
- A 100.000 filas: un retraso notable, pero tolerable.
- En filas de 1M: el hilo principal se congeló durante varios segundos. En casos raros, las pilas de llamadas recursivas profundas provocaban un desbordamiento de pila.
Agrupación: Misma raíz, coste compuesto
El agrupamiento extiende el mismo patrón recursivo y requiere ordenar primero los datos. De este modo, el coste del resolver se pagó una vez durante la clasificación y luego otra vez durante la detección de límites de grupo.
groupDataRecursive(data, state, level):
while i < data.length:
group = groupByExpression(data, i, expressions[level])
// resolver called once for group anchor value
// resolver called again for every subsequent record in the group
if level < expressions.length - 1:
groupDataRecursive(group, state, level + 1) // recurse into subgroups
else:
result = result.concat(...) // array allocation per group boundary
Aquí hay dos costes acumulados:
- El resolvedor de valores se invocaba repetidamente para valores que ya se habían resuelto durante la ordenación, sin caché compartida entre las dos fases.
- Cada límite de grupo producía nuevos arreglos mediante concat y slice, es decir, asignaciones que añadían presión de GC medible a gran escala a través de potencialmente miles de grupos
Filtrado al estilo Excel: Pagar el coste completo dos veces
El filtrado rápido y el filtrado avanzado eran rápidos. El filtrado al estilo Excel (ESF) no lo era, y la razón era arquitectónica.
Cuando se abría el diálogo ESF, activaba una pipeline de inicialización completa de forma sincrónica en el hilo principal:

La animación inicial del diálogo se pausó efectivamente hasta que se completaron las cuatro operaciones. Con grandes conjuntos de datos, esto era una congelación visible para el usuario, el diálogo no parecía torpe. Simplemente no apareció hasta que terminó la tubería.
El problema más crítico: toda esta canalización se ejecutaba de nuevo cuando el usuario hacía clic en Aplicar aunque los datos subyacentes no hubieran cambiado entre abrir y aplicar:
onApplyClick():
filter data
re-run full ESF initialization // same 4 steps, same cost, same blocking
close dialog
Por eso el ESF era significativamente más lento que el filtrado avanzado en la práctica: hacía lo mismoTrabaja dos veces por operación, bloqueando el hilo principal en ambas ocasiones.
Por qué "simplemente virtualizar más" no era la respuesta
La virtualización garantiza solo el número de filas visibles que se renderizarán como nodos DOM independientemente del tamaño del conjunto de datos. Eso es lo que hace posible desplazarse por 1 millón de filas. Pero las operaciones de datos que determinan qué contienen esas filas —ordenación, filtrado, agrupación— se ejecutan contra el conjunto de datos completo cada vez. La virtualización no puede ayudar ahí. Cada cuello de botella anterior permanecía en la tubería de datos, antes de que se renderizara una sola fila:
- Se llamó al resolver + Expresión por Orden, independientemente de cuántas filas se vieran.
- Agrupar pagaba de nuevo el coste de resolución además de la clasificación, además de la presión de asignación de concat/slice a través de los límites de los grupos.
- Toda la cadena de inicialización de ESF iteraba el conjunto de datos completo de forma sincrónica, al abrir y de nuevo al aplicar.
La virtualización es la herramienta adecuada para hacer que grandes cuadrículas sean desplazables. No ayuda en nada a que la ordenación, filtrado y agrupación sean rápidos. Esos requerían un tipo de reparación diferente.
Midiendo el problema: Cómo evaluamos el rendimiento de la red
Anécdotas como "se siente lento" y "parece rápido" son un punto de partida, no un diagnóstico. Para optimizar con confianza, necesitábamos números reproducibles en lugar de impresiones.
Es tentador confiar en los gráficos de llama o contadores de FPS de DevTools para diagnosticar el rendimiento de la cuadrícula. Pero esos miden toda la cadena de renderizado: detección de cambios, actualizaciones del DOM, diseño, lo que puede ocultar que el tiempo realmente se dedica a la canalización de datos.
Para identificar específicamente el coste del algoritmo, instrumentamos directamente la lógica de ordenación, agrupación y filtrado usando un envoltorio ligero alrededor de la API nativa de Rendimiento:
startMeasure(‘sorting’)
-> run sorting algorithm
getMeasures(‘sorting’) // returns the duration
Esto nos proporcionó un tiempo de análisis de algoritmos de forma aislada en submilisegundos sin generar ruido ni sobrecarga de detección de cambios. Solo el coste de la pipeline de datos en bruto. Cabe señalar: todos los números que aparecen a continuación se registraron en modo Angular desarrollador. Las compilaciones de producción serían más rápidas, pero la carga en modo desarrollo es consistente entre las ejecuciones, así que las diferencias relativas se mantienen.
Los conjuntos de datos
Rows:
10K / 100K / 1,000,000
Columns:
string - names, categories (with duplicates)
number - IDs, prices, quantities (with duplicates)
date - formatted date strings (require parsing)
time - HH:mm:ss formatted strings (require parsing)
La presencia de valores duplicados en las columnas de ordenación y grupo fue intencionada: refleja distribuciones realistas de datos e impacta directamente en el coste de agrupación, ya que más valores duplicados significan más detecciones de límites de grupo y llamadas recursivas más profundas. Las columnas de fecha y hora usaban representaciones de cadenas formateadas. Esto es importante para interpretar los resultados: toda comparación que involucra estas columnas requiere analizar la cadena en un valor comparable en tiempo de ejecución.
Scenarios and Results
En filas de 10K y 100K, la mayoría de las operaciones eran aceptables. Con un millón de filas, la imagen cambió drásticamente:
| Scenario | Tiempo (filas de 1M) |
| Single column sort – string | 3.38s |
| Single column sort – number | 1.50s |
| Multi-column sort – string → number | 3.88s |
| Agrupación – columna de una sola cadena (ordenar + agrupar) | 3.31s |
| Solo algoritmo de agrupación (después de ordenar) | 0.50s |
| Agrupación – dos columnas sobre la carga de la rejilla | 3.86s |
| Agrupación – dos columnas (después del ordenado) | 1.01s |
| ESF open – number column (15K unique values) | 1.60s |
| ESF open – date column (274 unique values) | 5.20s |
| ESF open – time column (86K unique values) | 6.60s |
| ESF apply – number column | 1.37s |
Lectura de los números
Varios patrones emergen de inmediato, y cada uno apunta directamente a un problema arquitectónico específico.
La clasificación domina el coste de agrupación. El algoritmo de agrupación por sí solo tardaba 0,50 s. Ordenación completa + grupo tardó 3,31 segundos – una diferencia de 6,6 veces. La lógica de agrupación en sí nunca fue el cuello de botella. Ordenación era, y específicamente el resolvedor de valores llamadoTiempos dentro del comparador de ordenación.
La ordenación de cadenas es más del doble de lenta que la ordenación de números (3,38s frente a 1,50s). Los números se comparan con una simple resta. Las cadenas pasan por el resolvedor de valores, la posible normalización para ordenaciones insensibles a mayúsculas y minúsculas y una comparación de cadenas. Esa diferencia se acumula en ~20 millones de comparaciones en filas de 1 millón.
La anomalía de fecha del ESF es el dato más revelador. La columna de fecha tenía solo 274 valores únicos, una lista diminuta comparada con los 15K de la columna numérica. Sin embargo, abrir el diálogo ESF tardaba 5,20 segundos frente a 1,60 segundos para la columna numérica. El culpable no era el recuento de iteraciones. Era el cálculo de fechas del coste por artículo. El conjunto completo de datos se iteró durante la inicialización de ESF, y cada valor pasó por análisis de cadenas hasta la fecha. Menos valores únicos no ayudaban porque el análisis se hacía en todos los registros, no solo en los únicos. La columna de tiempo (6,60 s con 86K valores únicos + análisis de cadenas temporales) confirma el mismo patrón: las columnas de cadenas formateadas son caras independientemente de la cardinalidad.
ESF abierto + ESF aplica = el coste total pagado dos veces. Para una columna numérica, el caso más barato es 1,60s + 1,37s = ~3s de bloqueo por operación de filtro. Para las columnas de fecha o hora, el coste combinado sería significativamente mayor.
Los números confirmaron lo que sugería la revisión de arquitectura: el resolvedor de valores, los pases de agrupación recursiva y la doble inicialización ESF eran los cuellos de botella. Ahora teníamos los datos para demostrarlo.
Optimización #1: Replanteando la Cadena de Clasificación
Con una línea de base clara establecida, el foco se desplazó hacia la propia cadena de datos. Tres cambios impulsaron la mayor parte de la mejora: aplicar la transformada Schwartziana a la ordenación, refactorizar la ordenación multicolumna de recursiva a iterativa, y reestructurar el algoritmo de agrupación para eliminar tanto la recursión como la asignación redundante de arrays.
Fix #1: The Schwartzian Transform
El comparador de ordenación original resolvía los valores de los campos dentro de la propia función de comparación, lo que significaba que para cada par de registros comparados, el resolvedor de valores se ejecutaba dos veces.
La transformada Schwartziana es una optimización clásica para claves de ordenación costosas: resuelve cada valor una vez por delante, ordena en los valores almacenados en caché y luego vuelve a mapear los registros originales. Esto mejora la resolución de campo desdePara:
// Before: resolve inside comparator - O(n log n) resolver calls
sort(data, field):
data.sort((a, b) => compare(resolveValue(a), resolveValue(b)))
// After: Schwartzian transform - O(n) resolver calls
sort(data, field):
prepared = data.map(record => [record, resolveValue(record, field)]) // O(n) - resolve once
prepared.sort(([, valA], [, valB]) => compareValues(valA, valB)) // O(n log n) — compare only
return prepared.map(([record]) => record) // O(n) - unwrap
El comparador se convierte en una comparación de valores pura sin resolución de campos, sin recorrido de caminos, sin análisis de fechas. Para ignoreCase, la llamada de normalización de cadenas pasa a la fase de mapeo – resuelta una vez por registro, no una vez por lado de comparación.
Para las columnas de fecha y hora, el impacto es especialmente significativo: el análisis de cadena a fecha pasa del bucle de comparador caliente a un único pase inicial. En 1 millón de filas esa es la diferencia entre ~40 millones de llamadas de análisis y exactamente 1 millón, que escon un multiplicador constante de 1, independientemente del tipo de columna.
Fix #2: Iterative Multi-Column Sorting
La ordenación original de varias columnas era recursiva: ordenar por expresión 0, encontrar grupos de iguales valores, ordenar recursivamente cada grupo por expresión 1, y así sucesivamente. Correcto, pero con dos problemas: profundidad de pila de llamadas recursivas y que el resolvedor de valores se llame de nuevo dentro de la detección de grupo para cada registro en cada pasada.
El nuevo enfoque itera hacia atrás a través de expresiones, lo que es una elección deliberada para mantener la estabilidad de ordenación, coincidiendo con el comportamiento de la implementación recursiva original:
// Before: recursive
sortDataRecursive(data, expressions, index):
sort by expressions[index]
for each equal-value group:
sortDataRecursive(group, expressions, index + 1) // recursive
// After: iterative - reverse pass maintains stability
sortData(data, expressions):
for i = expressions.length - 1 down to 0:
data = expressions[i].strategy.sort(data) // iterative, no recursion
Iterar al revés significa que la clave de ordenación más significativa se aplica al final. Se convierte en el desempate final y el orden general se mantiene estable. No hay pila de llamadas recursivas, ni pasos intermedios de detección de grupos entre expresiones; ni llamadas adicionales a resolvers. La transformada de Schwartz se aplica de forma independiente a cada paso de expresión.
Solución #3: Agrupación iterativa con una pila
El algoritmo de agrupación tenía dos fuentes de coste independientes: la estructura de llamadas recursivas y las asignaciones de arrays concat / slice en cada frontera de grupo. Ambos fueron dirigidos juntos.
// Before: recursive with concat/slice
groupDataRecursive(data, state, level):
group = data.slice(start, end) // allocation per group
result = result.concat(groupRow, group) // allocation per group
groupDataRecursive(group, state, level + 1) // recursive
// After: iterative with explicit stack + direct push
groupData(data, state):
stack = [{ data, level: 0 }]
while stack.length > 0:
{ data, level } = stack.pop()
for each group boundary in data:
result.push(groupRow) // no intermediate allocation
result.push(...groupRecords) // no intermediate allocation
if level < expressions.length - 1:
stack.push({ data: groupRecords, level: level + 1 })
La preasignación de los arreglos no era factible aquí porque el número de grupos no se conoce de antemano. Pero cambiar de concat / slice a direct push eliminó las asignaciones intermedias de arrays en cada frontera de grupo. A gran escala, a lo largo de potencialmente miles de límites de grupos, esto marcó una diferencia medible tanto en el tiempo de ejecución como en la presión de la GC.
Los resultados

Los milisegundos en bruto cuentan una parte de la historia. La métrica más importante es la percepción de la capacidad de respuesta:
- Un ordenamiento de cadena de columna única en 1M filas pasó de 3,38s –una congelación visible y brusca– a 0,42s, imperceptible para la mayoría de los usuarios
- La ordenación multicolumna bajó de 3,88s a 0,57s: los usuarios que aplican ordenación secuencial ya no experimentan retrasos compuestos
- La agrupación de dos columnas sobre la carga de la red pasó de 3,86 s a 0,88 s; la parrilla parece lista casi de inmediato
Las ganancias se acumulan en el uso real: un usuario que ordena, luego agrupa y luego vuelve a ordenar ya no espera varios segundos para cada una de esas operaciones. La tubería va lo suficientemente rápido como para que la interacción se sienta continua en lugar de interrumpida por congelaciones.
Optimización #2: Filtrado al estilo Excel a escala
La ordenación y agrupación eran los cuellos de botella más visibles, pero el filtrado al estilo Excel tenía sus propios problemas. El filtrado rápido y el filtrado avanzado operan directamente sobre los datos: un predicado se ejecuta sobre cada registro y devuelve una coincidencia. Simple, lineal, predecible.
El filtrado al estilo Excel es diferente. Antes de que el diálogo pueda mostrar algo, debe construir una imagen completa de los datos con cada valor único en la columna, formateado para mostrar, ordenado y cruzado con el estado actual del filtro. Eso no es solo una operación de filtrado. Eso es una pipeline de datos completa, y se ejecuta de forma sincrónica en el hilo principal cada vez que se abre el diálogo.
Como se mencionó antes, la inicialización original de filtrado al estilo Excel realizó cuatro pasadas secuenciales sobre los datos:
- Filtra el conjunto de datos si hay filtros aplicados de antemano – pass
- Ordena los valores filtrados –
- Extraer etiquetas + valores de formato – pass
- Deduplicate -> build unique items list – pass
La reinicialización de Apply fue la parte más desperdiciada: los datos subyacentes no cambiaron entre abrir y aplicar, pero toda la pipeline volvió a funcionar desde cero de todas formas.
Más allá del doble coste, la propia tubería tenía una ineficiencia: los pasos 2, 3 y 4 operaban todos sobre el conjunto de datos filtrado completo. La clasificación ocurría antes de la deduplicación, lo que significaba que la cuadrícula clasificaba potencialmente millones de registros cuando solo necesitaba ordenar los valores únicos. La extracción de etiquetas y la deduplicación también eran pasadas separadas sobre los mismos datos, visitando cada valor dos veces innecesariamente.
La anomalía de fecha y hora
La ineficacia era más evidente en las columnas de fecha y hora. De los puntos de referencia en Medición del problema:
| Columna | Unique values | Horario abierto del ESF |
| Número | 15k | 1.60s |
| Fecha | 274 | 5.20s |
| Tiempo | 86k | 6.60s |
La columna de fecha tenía 274 valores únicos —muchos menos que los 15K de la columna numérica— pero tardaba 3× más en abrirse. La razón: la extracción de etiquetas y el formato de valores implicaban análisis de fechas en todo el conjunto de datos, no solo en los valores únicos. Cada registro fue visitado, y cada visita activaba la conversión de cadena a fecha. Menos valores únicos no ayudaban porque el análisis se hacía durante el paso completo de datos, no después de la deduplicación.
Solución #1: Eliminar la doble inicialización
El cambio más impactante fue estructural: el ESF ya no se reinicializa en Aplicar. La lista de valores únicos construida en abierto se reutiliza directamente cuando el usuario hace clic en Aplicar. La segunda corrida completa del pipeline ha desaparecido por completo.
// Before
onApplyClick():
re-run full ESF initialization // O(n) - redundant
close dialog
// After
onApplyClick():
apply filter using existing list // O(1) - list already built
close dialog
Solución #2: Deduplicación de un solo paso con clasificación diferida
El segundo cambio reestructuró por completo la canalización, colapsando la extracción y deduplicación de etiquetas en una sola pasada, y luego ordenando solo el resultado deduplicado:
// Before: separate passes
filteredData → sort → extract labels (pass 1) → deduplicate (pass 2)
// After: deduplicate in single pass → sort unique list only
filteredData (n records)
→ single pass:
resolve + normalize + deduplicate inline // O(n), parse only for new unique values
→ unique list (m items)
→ sort unique list // O(m log m) where m <= n
Aquí hay dos mejoras que se suman a la sobra:
- El formato de etiquetas y el análisis de fechas ahora solo se ejecutan para valores únicos, no para todos los registros del conjunto de datos. Para una columna de fecha con 274 valores únicos en un conjunto de datos de filas de 1M, esa es la diferencia entre 1M de llamadas de análisis y 274.
- La ordenación ahora funciona sobre la lista deduplicada, no sobre el conjunto de datos filtrado completo. Con 274 valores únicos, la ordenación es efectivamente instantánea. Incluso para la columna de tiempos con valores únicos de 86K, ordenar 86K elementos es órdenes de magnitud más barato que ordenar 1M, y dado que cada comparación en ese ordenamiento implica un análisis de cadenas temporales, reducir la entrada de ordenación aumenta aún más el ahorro.
Fix #3: Non-Blocking Dialog Open
El tercer cambio abordó directamente el rendimiento percibido: el diálogo ahora se abre inmediatamente, antes de que se ejecute la cadena de datos. Se muestra un indicador de carga al finalizar la inicialización. Esto significa que la interfaz nunca se congela esperando un diálogo que aún no ha aparecido. Aunque la inicialización lleve tiempo, el usuario recibe una retroalimentación inmediata: el diálogo está abierto y algo está ocurriendo.
Fix #4: Debounced Quick Filtering
Una mejora menor pero significativa en el filtrado rápido: anteriormente, el filtro se activaba en cada pulsación de teclas, lo que significaba que un usuario que escribía "Finanzas" activaba 7 operaciones de filtro en rápida sucesión, cada una iterando el conjunto completo de datos.
// Before: filter on every keystroke input: "F" → filter // O(n) input: "Fi" → filter // O(n) input: "Fin" → filter // O(n) ... // After: debounced input: "F", "Fi", "Fin", "Fina", "Finan", "Financ", "Finance" → pause detected → filter once // O(n) - only when user stops typing
Para conjuntos de datos grandes, esto por sí solo reduce el número de operaciones de filtro en el hilo principal para una búsqueda típica de 5–10 a 1–2.
Los resultados

El número de aplicación ESF es especialmente significativo: con 90 ms, ahora está en el mismo rango de rendimiento que el filtrado rápido y el filtrado avanzado. Los tres modos de filtrado son ahora comparables en costes por primera vez.
Qué significa esto en la práctica
- El diálogo de ESF aparece inmediatamente al hacer clic. No más esperar a que aparezca un diálogo que no aparezca.
- El tiempo total para que los datos se carguen dentro del diálogo ESF es más rápido en todos los tipos de columnas. Los usuarios pasan menos tiempo mirando un indicador de carga incluso cuando el conjunto de datos es grande.
- Aplicar un filtro ya no repite el coste completo de inicialización. Es prácticamente gratis comparado con antes.
- El filtrado rápido ya no martilla el hilo principal de la escritura rápida. El rebote asegura que la tubería solo se ejecute cuando el usuario ha terminado o pausado.
Por qué estos cambios funcionan a través de marcos
Las mejoras de rendimiento mencionadas anteriormente se realizaron en la base de código Angular. Pero no se quedan allí.
One Core, Multiple Frameworks
La cuadrícula de Ignite UI está integrada en Angular– utilizable directamente como componente nativo de Angular con acceso completo a la sintaxis de plantillas, al sistema DI y a la detección de cambios de Angular. También está empaquetado como un componente web usando Angular Elements, lo que lo hace disponible fuera de Angular por completo. React y Blazor consumen ese Componente Web mediante wrappers delgados específicos del framework que conectan la API del elemento personalizado en React props y Blazor parámetros respectivamente.
La cadena de datos —ordenación, agrupación, filtrado— reside completamente en la base Angular. Angular Elements lo empaqueta en el Componente Web tal cual. React y Blazor nunca lo toque. Cada mejora algorítmica realizada en la base de código Angular se propaga automáticamente a lo largo de toda la cadena. Vale la pena ser preciso sobre lo que significa "envoltorio" aquí. Es una capa de integración delgada, no una reimplementación.
Por qué las mejoras en el algoritmo son independientes del framework
La transformada de Schwartz, la pila de agrupación iterativa y la deduplicación ESF de paso único son operaciones puramente de datos. Reciben un array y devuelven un array transformado. No tienen conocimiento de la detección de cambios de Angular, el reconciliador de React ni el árbol de renderizado de Blazor, y precisamente por eso se propagan tan limpiamente en las cuatro plataformas.
Las mejoras son ganancias del motor JavaScript:
- Fewer resolver calls per sort operation.
- Menos asignaciones intermedias de arrays por frontera de grupo.
- Less GC pressure across the full pipeline.
- Tiempo de bloqueo del hilo principal más corto en cada operación de datos.
Ninguno de estos son conceptos de marco. Un ordenamiento más rápido mejora el rendimiento independientemente de si el resultado se renderiza con Angular, React, Web Components o Blazor porque la optimización ocurre en la capa de datos antes de que el framework de la interfaz lo renderice.
Para los desarrolladores que evalúan qué red usar: la historia de rendimiento es la misma entre frameworks porque el motor es el mismo entre frameworks. Los números de esta publicación no son Angular números. Son números de pipeline de datos, y el pipeline de datos es compartido.
Qué significa esto para los equipos empresariales
Las victorias en rendimiento de ingeniería son fáciles de medir en milisegundos. Su impacto empresarial es más difícil de cuantificar, pero mucho más significativo, especialmente a escala empresarial, donde las redes de datos no son elementos decorativos de la interfaz de usuario, sino la interfaz principal a través de la cual analistas, traders y equipos de operaciones realizan su trabajo.
Los problemas de rendimiento en las redes de datos generan una categoría específica y frustrante de tickets de soporte: difíciles de reproducir, difíciles de diagnosticar y difíciles de cerrar. "La cuadrícula se congela cuando ordeno" no es un error con un trazado de pila. Es un síntoma de una tubería que bloquea el hilo principal durante varios segundos bajo volúmenes de datos reales.
Ignite UI soporta enlace remoto de datos con ordenación y filtrado que pueden delegarse a un servidor en lugar de ejecutarse en el lado del cliente. Para los equipos que adoptaron operaciones remotas principalmente porque el rendimiento en el lado del cliente era insuficiente, estas optimizaciones cambian el cálculo. La ordenación del lado del cliente en 1 millón de filas se completa ahora en menos de medio segundo. Para muchos conjuntos de datos empresariales que antes empujaban a los equipos hacia la delegación del lado del servidor, la canalización del lado del cliente es ahora lo suficientemente rápida como para reconsiderar esa decisión.
En entornos empresariales —especialmente en servicios financieros— la percepción de la capacidad de respuesta influye directamente en la adopción de la plataforma. Mover un tipo de 3,38s a 0,42s no es solo una mejora del 8× en aislamiento. Es la diferencia entre una interacción que interrumpe un flujo de trabajo y otra que no se registra como un retraso en absoluto. Esa distinción es importante cuando el usuario final decide si la herramienta merece la pena usarla.
Lecciones aprendidas: qué haríamos de nuevo (y de forma diferente)
Los números de antes y después de este post están limpios. El proceso que las produjo no lo era. Así es como fue realmente ese proceso.
Nada estaba garantizado desde el principio
Al comenzar este trabajo, no había ninguna certeza de que alguna de estas optimizaciones produjera resultados significativos. La transformada de Schwartz es una técnica muy conocida. Sin embargo, "conocida" no significa "garantizada para ayudar en este contexto". La pila de agrupación iterativa parecía prometedora sobre el papel, pero los refactores recursivos a iterativos tienen una historia de introducir casos límite sutiles que solo aparecen bajo formas de datos específicas.
El enfoque era deliberadamente incremental: abordar un problema a la vez, una medida, y luego decidir si continuar. La cadena de selección fue la primera. Cuando los números regresaron – de 3,38 a 0,42 segundos en un ordenamiento de cadena – validaban la dirección y justificaban continuar con el agrupamiento y filtrado. Si la primera optimización hubiera mostrado ganancias marginales, la estrategia habría cambiado.
Esto importa porque el trabajo de interpretación suele planificarse como si los resultados se conocieran de antemano. No lo son. La postura correcta es hipótesis, medición, decisión, repetición.
El Intercambio de la Memoria
La transformación de Schwartz no es gratis. Asigna un array intermedio de pares [registro, valor] de entrada: una entrada por registro. En filas de 1 millón, eso es una sobrecarga de memoria no trivial antes de que empiece el orden.
Esto era un intercambio consciente: aceptar un mayor consumo de memoria a cambio de eliminarLlamadas de resolver. Para los casos de uso a los que se dirige esta biblioteca, es decir, grids empresariales ejecutándose en navegadores modernos en hardware compatible, las ganancias de velocidad son significativas y el coste de memoria es aceptable.
Pero vale la pena mencionarlo explícitamente: si los entornos con limitaciones de memoria llegaran a ser un objetivo principal, la transformación schwartziana tendría que ser revisitada. Aquí la velocidad y la memoria tiran en direcciones opuestas, y la implementación actual eligió velocidad.
Los benchmarks deben reflejar el uso real
El conjunto de benchmarks para este trabajo utilizó conjuntos de datos sintéticos en 1M de filas (registros generados con tipos de columnas y distribuciones de valores controlados). Ese es el punto de partida adecuado para aislar el rendimiento algorítmico, pero tiene un límite.
Los dos problemas que realmente motivaron este trabajo vinieron de un cliente real: el tiempo de apertura del diálogo ESF y el tiempo de aplicación ESF se reportaron como problemas de bloqueo en producción. Cuando llegaron esos tickets, los benchmarks sintéticos confirmaron el problema. El problema ya existía antes de la multa. Hizo falta un patrón de uso real para que se manifestara.
La lección es sencilla: los benchmarks sintéticos son buenos para medir escenarios que ya sabes probar. Los datos de clientes encuentran aquellos que no pensaste en incluir. Ambos son necesarios, y el conjunto de benchmarks debería evolucionar para incorporar patrones de uso reales a medida que surjan, no solo en los peores casos sintéticos.
El trabajo de performance nunca se termina
Las mejoras en esta publicación son reales y significativas. También son una instantánea. La cadena de datos es hoy más rápida que hace seis meses. Dentro de seis meses, hay áreas conocidas, como el análisis de fechas, la virtualización, etc., que se parecerán a la pipeline de ordenamiento que se veía antes de este trabajo. Serán funcionales, pero con margen de mejora que aún no se ha abordado.
Eso no es un fracaso del trabajo actual. Es la naturaleza de la ingeniería de rendimiento. La línea base se mueve, los volúmenes de datos de los clientes crecen y la definición de "lo suficientemente rápido" se desplaza con ella. El valor de esta ronda de optimizaciones no son solo los milisegundos ahorrados. Es el proceso establecido para encontrar y cerrar la siguiente brecha.
¿Qué viene para Ignite UI rendimiento en la red eléctrica
Las optimizaciones de esta publicación representan una ronda enfocada de trabajo de rendimiento y no una declaración final sobre el tema. Ya hay varias áreas en marcha y se están explorando activamente más.
Lo que ya ha mejorado
El rendimiento de la virtualización ha mejorado junto con el trabajo de ordenación, agrupación y filtrado que se trata en esta publicación. La virtualización de filas y columnas es la base que hace posible la renderización de grandes bases de datos. Todas las mejoras ahí se suman a las ganancias de la tubería de datos, lo que significa que la red es más rápida tanto procesando datos como renderizándolos.
¿En qué se sigue trabajando
El análisis de dátiles sigue siendo un área con margen de mejora conocido. Los resultados de ordenación y ESF para las columnas de fecha y hora son mucho mejores que antes, pero siguen siendo más lentos que las columnas numéricas en aspectos que se remontan a cómo se analizan las cadenas de fechas. El siguiente paso lógico es un trabajo más específico en la capa de analización.
El tamaño del paquete es un tema constante. Una cuadrícula más rápida que distribuya más JavaScript del necesario juega en contra, especialmente para equipos donde el tiempo de carga inicial es tan importante como el rendimiento en tiempo de ejecución. Reducir la huella de la red sin sacrificar capacidad es un acto de equilibrio continuo.
El refinamiento de la API de cuadrícula continúa en paralelo. No es un problema directo de rendimiento, sino que está relacionado con ello. Una API más limpia reduce el área superficial donde se invocan rutas de código sensibles al rendimiento de formas no deseadas.
El rendimiento en tiempo de ejecución en general, incluyendo el coste de renderizado, la presión de detección de cambios y la respuesta a la interacción bajo actualizaciones de alta frecuencia, sigue siendo un área abierta de exploración. No hay afirmaciones concretas, pero está en el radar.
Comparte tus comentarios sobre el rendimiento
Cada mejora de rendimiento eleva la base y las expectativas. Lo que antes era lento se vuelve rápido y finalmente aparecen nuevos cuellos de botella.
Por eso valoramos la retroalimentación útil del uso real. Si usas Ignite UI grids en producción y tienes problemas de rendimiento, abre un problema en GitHub. Los escenarios reales y los casos reproducibles nos ayudan a identificar las próximas oportunidades de mejora.
Cierre: El rendimiento como promesa, no como punto clave
Cada librería de cuadrícula indica el rendimiento como una característica. "Gestiona millones de filas" aparece en tablas comparativas junto a otras características como una casilla de verificación, no un compromiso.
Hay una diferencia entre una cuadrícula que técnicamente gestiona grandes conjuntos de datos y otra que los maneja sin hacer esperar a los usuarios. Esa diferencia no aparece en una lista de características. Aparece cuando un usuario hace clic en una cabecera de columna o abre un diálogo de filtro y recibe una respuesta inmediata o ve cómo la interfaz se congela.
El trabajo de esta publicación estuvo impulsado por esa distinción. No por un requisito de marketing, sino por un cliente real que se topa con muros reales de rendimiento, y por el reconocimiento de que "funciona" y "es rápido" no son la misma afirmación. La transformada Schwartziana, la pila de agrupación iterativa, la tubería ESF de paso único – nada de eso era evidente desde el principio, nada estaba garantizado para funcionar, y todo requería medición para justificarse.
El rendimiento no es una característica que se lanza y se deja atrás. Es una obligación continua hacia los desarrolladores y usuarios finales que dependen de estos componentes para hacer un trabajo real, a gran escala, sin que la interfaz interfiera.
Tenemos la intención de seguir cumpliéndola.