Cómo Mostrar 10 Millones de Algo: Rendimiento del Frontend Más Allá de la Memoización

This ad is not shown to multipass and full ticket holders
JSNation US
JSNation US 2025
November 17 - 20, 2025
New York, US & Online
See JS stars in the US biggest planetarium
Learn More
In partnership with Focus Reactive
Upcoming event
JSNation US 2025
JSNation US 2025
November 17 - 20, 2025. New York, US & Online
Learn more
Bookmark
Rate this content

Cuando se habla de rendimiento en el frontend, generalmente hay dos temas: las puntuaciones de Lighthouse y los rerenders. Pero cuando se trabaja en aplicaciones que manejan grandes cantidades de datos y la paginación no es una opción, se vuelven necesarias categorías de optimización completamente diferentes.

A través del estudio de caso del visor de trazas de Axiom, examinaremos las soluciones que mantienen tu aplicación funcionando (¡y funcionando rápido!) cada vez que el tamaño de tus datos crece en un orden de magnitud. 

This talk has been presented at React Day Berlin 2024, check out the latest edition of this React Conference.

Christopher Ehrlich
Christopher Ehrlich
29 min
16 Dec, 2024

Comments

Sign in or register to post your comment.
Video Summary and Transcription
La charla de hoy se centró en la optimización del rendimiento en aplicaciones de React, específicamente en el manejo y renderizado de enormes cantidades de datos. El orador discutió varias técnicas y herramientas para lograr un mejor rendimiento, como la optimización de la transmisión de datos, el renderizado, el procesamiento de datos, el uso de memoria y la desnormalización. También destacaron la importancia de optimizar el rendimiento de la red, el tamaño de la carga útil y el manejo de solicitudes. La charla enfatizó la necesidad de medir antes de optimizar, centrarse en los cuellos de botella y hacer pequeñas mejoras que realmente beneficien a los usuarios. En general, la charla proporcionó valiosos conocimientos y recomendaciones para mejorar el rendimiento en aplicaciones de React.

1. Performance in React Apps

Short description:

Hoy, hablaré sobre el tipo específico de rendimiento que estaba buscando en aplicaciones de React. Si bien los indicadores web son importantes, a los usuarios les importa más el rendimiento durante el uso. El rendimiento de renderizado se puede optimizar con recursos como los escritos de Nadia Makarovic y el próximo React Compiler. Para cálculos pesados en tiempo de ejecución, puedes explorar RustWasm. Me centraré en manejar enormes cantidades de datos. Comenzaremos con una demostración de Axiom's Trace Viewer, una aplicación de alto rendimiento con más de un millón de spans. Ahora, hablemos de cómo lograr resultados similares.

Hola, hoy voy a hablar sobre el rendimiento en aplicaciones de React. Ahora, cuando comencé a trabajar en el proyecto que discutiré en esta charla, encontré que hay muchos recursos sobre el rendimiento de React y el rendimiento del frontend en general. Pero la mayoría de ellos no eran realmente tan relevantes para el tipo específico de rendimiento que estaba buscando.

Probablemente la conversación más común sobre el rendimiento es la de los indicadores web. Ahora, los indicadores web son súper importantes. Determinan qué tan rápido carga tu aplicación, qué tan rápido es interactiva, y así sucesivamente. Y hoy en día, hay muchas soluciones excelentes para esto. Si estás en el ecosistema de React puedes probar Next.js o Remix. Y también hay muchas otras herramientas excelentes, como Astro o Quick. Sin embargo, principalmente trabajo en lo que llamaría aplicaciones o paneles, así que encuentro que a los usuarios no les importa tanto qué tan rápido carga el sitio siempre que sea razonablemente rápido. Y les importa mucho más cómo funciona una vez que lo están usando.

Eso nos lleva al segundo tema que se discute con bastante frecuencia, que es el rendimiento de renderizado. Este es un tema con el que trato bastante a menudo, pero encuentro que hay muchos recursos excelentes sobre esto hoy en día. Por ejemplo, recomendaría los escritos de Nadia Makarovic. Descubrí que una vez que sabes cómo optimizar React un poco, se vuelve bastante rápido, y todo el problema del rendimiento de renderizado está mayormente resuelto. Además, React Compiler llegará pronto, lo que nos ayudará aún más con esto. Y luego, por supuesto, hay otros frameworks que se centran aún más en el rendimiento de renderizado, como Solid.js.

Otro tema sobre el que ya hay bastantes recursos es el de tener cálculos muy pesados en tiempo de ejecución. Y hay muchas cosas que puedes hacer aquí. Una de ellas, por ejemplo, es escribir RustWasm, y si estás interesado en ese tipo de cosas, entonces Ken Wheeler tiene algunas charlas realmente buenas sobre este tema. Pero el tipo de rendimiento con el que me encontré luchando es cómo manejar enormes cantidades de datos, decenas de miles, cientos de miles o incluso millones de elementos. Ahora, como muestra este tweet aquí, creo que mucha gente no entiende por qué querrías hacer esto, y es una pregunta muy razonable, por lo que quiero hablar de ello hoy. Así que, lo que estamos viendo hoy es cómo mostrar 10 millones de algo.

Comencemos con una demostración. La aplicación que vamos a ver hoy es Axiom's Trace Viewer. Tiene un conjunto de características bastante normal para este tipo de aplicación. Así que, por ejemplo, puedes pasar por spans de error, puedes ver los detalles de varios spans, y si un span es particularmente lento, puedes echar un vistazo a cómo se compara con los spans habituales de ese tipo. Por supuesto, también tiene muchas otras características, pero el punto principal que quiero mostrar es que es bastante eficiente, aunque actualmente tenemos un Trace Open con poco más de un millón de spans, y esto es en localhost, que tiene bastante sobrecarga en comparación con ejecutar la aplicación en producción.

Así que, ahora que has visto la aplicación, hablemos de cómo puedes lograr algo como esto. Algunos detalles rápidos sobre mí.

2. Handling 10 Million Items

Short description:

Mi nombre es Chris. Recomiendo evitar manejar 10 millones de elementos ya que toma mucho tiempo y hace que la base de código sea más difícil. Escucha a tus clientes y desarrolla contra datos reales de una escala similar. Usa paginación, transmisión o agregación. Negocia los requisitos si te piden mostrar millones de elementos. Mide antes de optimizar, enfocándote en computación, memoria y red. Reconsidera la arquitectura para mejorar la capacidad. Los errores iniciales no se originaron en el front end.

Mi nombre es Chris. Trabajo para Axiom, que es la aplicación que acabas de ver, y en el mundo de código abierto, soy conocido por algunas contribuciones a CreateT3App y TRPC y algunos otros proyectos.

Entonces, ¿cómo manejas 10 millones de elementos? Bueno, mi recomendación es simplemente no hacerlo. Ahora, por supuesto, este no es un consejo muy útil, pero quiero ser serio aquí. Intenta evitarlo. Este tipo de trabajo toma mucho tiempo que podrías gastar en arreglar errores, escribir nuevas características, y así sucesivamente, y también hace que tu base de código sea mucho más difícil para la próxima persona que trabaje en ella.

Creo que a muchos de nosotros nos encanta esta idea de resolver problemas tecnológicos muy interesantes, pero lo que necesitas pensar es, ¿es esta la mejor manera de gastar tu tiempo para hacer la vida de tus usuarios mejor? La forma de saber si necesitas esto o no es escuchar a tus clientes y ver cuáles son sus frustraciones. La otra cosa que puede ayudarte a averiguar si necesitas esto o no es desarrollar consistentemente contra un servidor real con datos reales de una escala similar a la que tienen tus usuarios más grandes.

Entonces, si debes evitarlo, ¿cómo puedes hacerlo? Va a depender de tu situación, pero hay muchas opciones aquí. Puedes usar paginación, obteniendo 10, 20 o 100 resultados a la vez. Puedes usar transmisión, obteniendo solo los resultados específicos que actualmente se necesitan. O tal vez ni siquiera necesitas los elementos individuales, solo necesitas ejecutar algún tipo de agregación sobre ellos. En ese caso, puedes hacerlo del lado del servidor, o en muchos casos, incluso en la base de datos. Y la última cosa que quiero señalar es que si tu propietario de producto pide esto, realmente sugeriría negociar los requisitos y averiguar por qué quieren esto. Tal vez te estén presentando un problema XY, y realmente hay una solución mucho mejor, como una de las tres cosas anteriores.

Pero digamos que has negociado, has pensado en esto, has considerado las alternativas, y has llegado a la conclusión de que la única manera de hacer que tu aplicación sea buena es mostrar millones de elementos a la vez. ¿Qué haces ahora? Lo primero que es muy importante es medir antes de comenzar a optimizar. Hay una buena posibilidad de que estés completamente equivocado sobre dónde están tus cuellos de botella. Hay tres cosas principales para medir, computación, memoria y red, y veremos cada una de ellas más adelante.

Así que ahora que hemos pasado la introducción, hablemos sobre las cosas específicas que nos ayudaron en nuestra situación. Voy a omitir algunos detalles de implementación aquí, así que mostraré algunas cosas que no representan uno a uno cómo se comporta Axiom en producción. Pero estos detalles son muy específicos de nuestra situación, y creo que terminaremos con una charla más útil de esta manera.

Así que aquí está el punto de partida. Es finales de 2023, y el trazado en Axiom funciona muy bien, hasta alrededor de 5000 spans. Pero no sabíamos esa última parte en ese momento, porque ¿por qué alguien querría crear trazas tan grandes? Y luego lanzamos un nuevo plan de precios, y la gente se dio cuenta de que podían usar nuestro trazado como una herramienta de perfilado sin incurrir en grandes costos, y así lo hicieron, y no pudimos manejarlo. Esto es lo que sucedió si intentabas abrir una traza con incluso solo 10,000 spans. Creo que puedes ver por el mensaje de error que ni siquiera habíamos considerado esta posibilidad o esta forma de fallar.

Después de una rápida investigación, nos dimos cuenta de que realmente teníamos que repensar toda la arquitectura del visor de trazas. Así que veamos lo que hicimos paso a paso para mejorar nuestra capacidad en varios órdenes de magnitud. Ahora, este conjunto inicial de errores no se originó realmente en el front end.

3. Optimizing Data Relay

Short description:

Dejamos de retransmitir los datos a través del back-end para front-end y hicimos que el front-end hablara directamente con el back-end principal para grandes cantidades de datos.

Teníamos un pequeño servidor node que estábamos retransmitiendo los datos a través de él, que se comunicaba con nuestro back-end real, que está escrito en Golang. Probablemente podríamos haber seguido optimizando ese servidor node, pero rápidamente nos dimos cuenta de que los requisitos no eran solo el doble o diez veces más altos de lo que actualmente soportábamos, sino probablemente más cerca de mil o incluso diez mil veces. Así que lo primero que tuvimos que hacer fue dejar de retransmitir los datos a través del back-end para front-end.

Tener un BFF puede ser genial. Se despliega junto con el front-end, ayuda con la seguridad, la desincronización de versiones y muchas otras cosas, pero no es la solución adecuada para cada situación, y claramente no lo era aquí. La solución aquí es simple, ¿verdad? En lugar de retransmitir a través del BFF, simplemente hablas directamente con el back-end. Pero esto trae algunas dificultades consigo. Por ejemplo, podrías necesitar replantearte cómo funcionan la autenticación y la seguridad. Pero para grandes cantidades de datos, se vuelve muy importante canalizarlos a través de la menor cantidad de lugares posible. Así que cambiamos la arquitectura para que el front-end hablara directamente con el back-end principal para este caso de uso, y esto funcionó muy bien.

4. Optimizing Rendering and Data Processing

Short description:

Para optimizar el rendimiento de renderizado en React, virtualiza los datos renderizando solo las filas en el viewport. Dos bibliotecas recomendadas para esto son Virtua para una solución pequeña y ligera, y Tanzac Virtual para una opción más potente y configurable. Para investigar el bloqueo de cómputo, utiliza la pestaña de rendimiento en las Chrome DevTools y concéntrate en el tiempo propio. Busca patrones de código reconocibles y familiarízate con nombres comunes en tu aplicación. Para mejorar el rendimiento al virtualizar una lista, mueve el procesamiento de datos al componente de renderizado, incluso si agrega una ligera sobrecarga.

Pero muy rápidamente, comenzamos a encontrar otros límites, y el primero fue el rendimiento de renderizado. Esto probablemente no sea sorprendente si has intentado renderizar miles o decenas de miles de elementos en React antes, pero no va muy bien. La buena noticia es que este es el más fácil de resolver, y la solución es virtualizar los datos. Todo lo que significa es que en lugar de renderizar cada fila individual, solo renderizas las que están en el viewport y un par por encima y por debajo para hacer el desplazamiento suave.

Y hay dos bibliotecas que recomendaría para hacer esto. Si quieres algo que sea muy pequeño y ligero, menos de 2 kilobytes de tamaño de paquete, y básicamente plug and play, entonces recomendaría la biblioteca Virtua. Y si estás buscando algo más potente y configurable, entonces Tanzac Virtual es una excelente opción. Esto resolvió nuestro renderizado real, pero todavía teníamos mucho cómputo que estaba bloqueando la aplicación. Veamos cómo investigar eso.

La herramienta principal para esto es la pestaña de rendimiento en las Chrome DevTools. Antes de entrar en cómo analizar esto, hay dos números mágicos que quiero que conozcas. El primero es 16 milisegundos, que es un cuadro a 60 hertz. Así que si bloqueas por más de 16 milisegundos, vas a perder un cuadro. El segundo número es 200 milisegundos, que es aproximadamente la cantidad máxima de tiempo que puedes bloquear en un clic antes de que empiece a sentirse bastante mal para el usuario.

Escribir, por supuesto, siempre debe mantenerse a 60 cuadros por segundo, pero especialmente si necesitas hacer solicitudes de red, los usuarios tienden a perdonar latencias de hasta 100 o en la mayoría de los casos, incluso 200 milisegundos. En términos de cómo leer este diagrama, lo que más te importa es el tiempo propio, que es cuánto tiempo se pasa no en los hijos del span actual, sino en el span mismo. Sin embargo, si te desplazas hacia abajo, principalmente terminarás en funciones de biblioteca o llamadas al sistema y cosas así. Así que en ese punto, querrás volver arriba y encontrar lo último que reconozcas o que sea parte de tu propio código base.

Una regla general rápida aquí es buscar lo último azul. Pero si tienes mapas de origen, puedes hacerlo mucho mejor que eso. Y puedes ver en la parte inferior de la pantalla aquí, está haciendo referencia a la función parse.ts, que es parte de nuestro propio código base. Y si en su lugar está haciendo referencia a una función dentro de una biblioteca, el nombre del archivo comenzaría en su lugar con node modules. Pero también habiendo dicho eso, no hay un truco rápido que te permita entender cada gráfico de cuadros inmediatamente. Así que si esto es algo que haces a menudo, realmente deberías acostumbrarte a hojear estos de arriba a abajo y encontrar los patrones que son relevantes para tu aplicación y familiarizarte con los nombres de las cosas que suceden bastante a menudo en tu aplicación.

Ahora, el problema de rendimiento que teníamos en nuestra aplicación era que ahora estábamos virtualizando la lista, pero en realidad todavía estábamos procesando todos los datos por adelantado. Estoy seguro de que has visto un ejemplo como el componente de la izquierda antes, donde obtienes una lista de elementos, y luego los procesas, y luego los renderizas uno por uno. El problema aquí es que si tienes un millón de elementos, entonces necesitas procesar un millón de elementos incluso si solo estás renderizando cinco de ellos.

La solución más común aquí es mover el procesamiento de los datos al componente que hace el renderizado para una pieza de los datos. Esto significa que la aplicación funcionará un poco más lenta después de la carga inicial, porque cada vez que se renderiza una nueva fila, necesita procesar los datos para ella. Pero usualmente esa sobrecarga es insignificante.

5. Optimizing Memory Usage and Denormalizing Data

Short description:

Después de optimizar el cómputo, enfrentamos un problema de memoria al manejar una gran cantidad de spans. Usar MobX para la gestión de estado causó problemas de rendimiento debido a millones de objetos anidados. Cambiar a primitivas de JavaScript mejoró el rendimiento pero planteó desafíos con objetos arbitrariamente grandes. Para abordar esto, desnormalizamos los datos y cargamos los spans de manera incremental en lugar de enviar todos los datos de una vez.

Y eso fue en realidad, en nuestro caso, toda la optimización que tuvimos que hacer en términos de cómputo. Pero había otro problema mucho, mucho más grande. Después de una cierta cantidad de spans, la aplicación simplemente se quedaba sin memoria. Esto tiene sentido ya que cada elemento adicional requiere memoria adicional, y simplemente no había suficiente para todos.

La memoria en Chrome es un poco complicada, pero una regla general que uso es que generalmente puedes obtener cuatro gigabytes si eres la aplicación activa, pero realmente deberías intentar mantenerte por debajo de un gigabyte si es posible. Y la forma de saber cuánto estás usando es abrir la pestaña de memoria en las herramientas de desarrollo y tomar una instantánea de memoria. En este caso, hemos abierto un rastro con 8,210 spans, y puedes ver que la instantánea de memoria ocupa 750 megabytes. Así que solo podíamos manejar tal vez 30,000 o 40,000 spans antes de quedarnos sin memoria.

Con una arquitectura así, no vamos a llegar a 10 millones de spans. La pestaña de memoria en las Chrome DevTools es bastante poderosa, pero para dar un curso intensivo lo que a menudo hago es ordenar por tamaño retenido y luego profundizar en eso y buscar la primera cosa que reconozco. Y luego en la mitad inferior de la herramienta, puedes ver qué está usando realmente esa cosa. Así que en nuestro caso, puedes ver que aunque solo tenemos alrededor de 8,000 spans, hay 1.9 millones de estos valores observables y se usan abrumadoramente en el almacén de Traces.

Esto hizo que la causa del problema fuera bastante clara. Estábamos usando MobX como nuestra solución de gestión de estado en el front-end para tener reactividad de grano fino en la UI. Pero la forma en que MobX funciona es que envuelve un proxy alrededor de cada objeto, lo cual es generalmente una muy buena solución. Pero en este caso particular, se vuelve costoso muy rápido si tienes millones de objetos profundamente anidados. Experimentamos con otras soluciones de gestión de estado o incluso solo usando el estado incorporado de React pero realmente no estábamos contentos con ninguna de ellas. Así que lo que decidimos hacer es simplemente usar primitivas de JavaScript.

Manteníamos los spans, que son objetos grandes, fuera de MobX e incluso fuera de React, en lugar de solo usar objetos regulares de JavaScript, y manteníamos algo mucho más pequeño en MobX que era solo suficiente para hacernos saber cuándo actualizar la UI. Ahora, por supuesto, el estado de React y otras soluciones de gestión de estado están construidas por una razón, ¿verdad? No queremos tener que decirle a nuestra aplicación cuándo volver a renderizar la UI. Sin embargo, en este caso, nos dimos cuenta de que realmente no necesitamos la reactividad más fina, ya que obtenemos todos los spans de una vez, y luego solo necesitamos notificar a la aplicación que los tenemos. Y una vez que están cargados, no van a cambiar. Mantener las cosas costosas en JavaScript puro fue enormemente exitoso. De repente teníamos soporte para alrededor de 100,000 spans, o en algunos casos incluso 200,000 o 300,000. Pero el problema es que los spans pueden tener un número arbitrario de atributos, y como puedes imaginar, un par de millones de objetos arbitrariamente grandes se vuelven costosos incluso si no tienes la sobrecarga de una solución de gestión de estado. Además, cargarlos a través de la red estaba tomando una eternidad. Así que necesitábamos dar un paso más. Hasta ahora el servidor había estado enviando todos los datos para todos los spans, y luego usábamos eso para renderizar la UI. Y eso simplemente ya no era factible.

6. Denormalizing Data and Optimizing Loading

Short description:

Para mejorar el rendimiento de lectura y simplificar la ejecución de consultas, desnormalizamos el modelo de datos en el frontend. El visor de trazas consta de un árbol de spans y detalles de spans, con la vista de cascada solo requiriendo algunos atributos. Optimizamos la carga enviando solo la información necesaria para los spans y obteniendo los detalles de los spans según sea necesario. Implementamos una precarga al pasar el cursor usando react-query y utilizamos el ID del span en el DOM para evitar pasarlo al controlador de eventos. Se implementaron diferentes comportamientos para trazas más pequeñas y más grandes, incluyendo búsqueda de texto completo para spans completos y solicitudes de red con parámetros de búsqueda para la información del árbol de spans. Este enfoque resultó en cinco tipos de solicitudes, con Axiom sirviendo como la API de base de datos.

Lo que necesitábamos hacer era desnormalizar. La desnormalización significa introducir redundancia en un modelo de datos, típicamente en la base de datos, pero en nuestro caso, en el front end, para mejorar el rendimiento de lectura o para simplificar la ejecución de consultas. Más específicamente, si piensas en el visor de trazas, hay dos partes en él. El árbol de spans a la izquierda y los detalles de los spans a la derecha. Ahora, los detalles de los spans a la derecha realmente necesitan todos los atributos porque necesitas poder desplazarte por todos los detalles del span, pero resulta que la vista de cascada a la izquierda solo necesita algunos atributos. Si piensas en lo que muestra la cascada, necesitas saber el nombre del span y el servicio que lo emitió, necesitas saber su hora de inicio y duración, y necesitas saber el id y el id del padre para poder crear una jerarquía de spans. Pero eso es todo. No necesitas ninguno de los otros atributos.

Así que lo que empezamos a hacer es enviar solo esas cosas necesarias para todos los spans, y luego obtener el detalle para cada span en la barra lateral según sea necesario. Por supuesto, tener un indicador de carga en la barra lateral no es ideal, así que también necesitábamos una solución para eso. Lo que terminamos haciendo fue una precarga bastante simple al pasar el cursor usando react-query. Una vez que el cursor entra en el span, esperamos 300 milisegundos, y si el cursor todavía está sobre el span, obtenemos sus detalles. Un truco ingenioso que usamos aquí es poner el id del span en un atributo en el DOM mismo para no necesitar pasar el id del span al controlador de eventos. Esto significa que todas las filas comparten un handle mouse enter, un handle mouse leave, y un timeout ref. Para llevar esto más allá, también puedes hacer cosas interesantes como precargar los resultados de búsqueda siguientes y anteriores o los spans de error siguientes y anteriores para que los usuarios puedan pasar por ellos rápidamente.

Pero, por supuesto, todavía habrá situaciones donde esto se comporte peor, por ejemplo, si el usuario pasa por errores o resultados de búsqueda bastante rápido, o si hace clic muy rápido. Así que lo que estoy mostrando aquí no es realmente toda la verdad. Lo que realmente hicimos es, dependiendo del tamaño de la traza, cargaríamos el span completo o solo la información necesaria para la cascada. Esto significa que la mayoría de nuestros usuarios que manejan trazas pequeñas tienen el rendimiento en tiempo real, pero para los usuarios que necesitan que podamos manejar trazas enormes, también podemos hacerlo. Otro ejemplo de donde hay un comportamiento diferente para trazas más pequeñas y más grandes es en la búsqueda. Así que cuando tenemos spans completos, hacemos una búsqueda de texto completo en JavaScript, pero cuando solo tenemos la información necesaria para el árbol de spans, por supuesto, no podemos hacer eso porque no tenemos ninguno de los otros atributos. Así que lo que hacemos en ese caso es enviar una solicitud de red con los parámetros de búsqueda y luego devolver una respuesta con todos los IDs de spans que coinciden con la búsqueda. Y una vez que todo eso está implementado, aquí están los cinco tipos de solicitudes con los que terminamos. Ahora esta diapositiva es un poco de una fabricación porque en realidad no usamos una API REST tradicional para nada de esto. En su lugar, usamos Axiom, la base de datos, como nuestra API. Pero si estuvieras construyendo una API REST para comportarse como lo que estamos haciendo, entonces se vería algo así. Lo primero que necesitarías es un endpoint que te proporcione la información de una traza basada en su ID de traza. Y todo lo que eso significa es que tiene una hora de inicio de x, una hora de finalización de y, y tiene un cierto número de spans. Basado en el número de spans, sabes si es una traza pequeña o grande, y si deberías obtener los detalles completos del span o solo lo necesario para la cascada. Y las horas de inicio y finalización pueden ayudar enormemente con el rendimiento de las consultas si tu tabla tiene un índice en la marca de tiempo.

7. Optimizing Data Fetching and Network Performance

Short description:

Implementamos los endpoints necesarios para devolver spans, manejar la búsqueda y gestionar la visualización y filtrado de spans y resultados de búsqueda. Al optimizar el uso de memoria y utilizar la desnormalización, pudimos mostrar un millón de spans a la vez. Sin embargo, aún enfrentamos desafíos con errores de falta de memoria al mapear sobre grandes conjuntos de spans. Para abordar esto, recomendamos usar un bucle for y procesar manualmente cada elemento. Además, optimizamos las operaciones de arrays usando push y pop en lugar de shift y unshift. El mayor desafío restante es obtener eficientemente los datos reales, a pesar de la desnormalización. Comprender las solicitudes de red y su rendimiento se puede hacer usando la pestaña de iniciador en la pestaña de red de Chrome DevTools, que proporciona información sobre la fuente de las solicitudes de red.

Luego, por supuesto, necesitamos los dos endpoints que devuelven los spans. Así que o bien obtienes una traza completa y eso solo toma un ID de traza, o en el caso de obtener la cascada de la traza, también pasas un ID de traza y luego obtienes lo que es necesario para la cascada. En nuestro caso, en realidad agrupamos esto, así que también proporcionamos un conteo y un desplazamiento. Si no estás obteniendo spans completos, entonces necesitas un endpoint separado que te proporcione todos los detalles de un span. Esto se usa para completar los detalles del span a la derecha de la pantalla.

Finalmente, necesitas el endpoint que maneja la búsqueda. Esto toma un ID de traza y algún texto de búsqueda y devuelve una lista de IDs de spans que coinciden con la búsqueda. Esta es la arquitectura con la que terminamos en JavaScript. Lo primero que tenemos son los spans, y esto en nuestro caso es un objeto donde la clave es el ID del span y el valor es el span en sí. La razón por la que lo hacemos de esta manera es para que acceder a cualquier span sea O de 1. La segunda cosa que tenemos se llama el árbol de IDs de spans, y la razón por la que necesitamos esto es porque los spans tienen padres e hijos, y necesitamos poder recorrer esa jerarquía rápidamente. Así que construimos esto donde cada objeto solo tiene el ID del span y la información sobre sus hijos, y luego si necesitamos conocer los detalles sobre un span específico, lo obtenemos del objeto de spans.

Lo primero que tenemos en MobX se llama spans filtrados, y esto es solo un array de IDs de spans que representa el orden en que el span debe ser renderizado en el visor de trazas. Los usuarios pueden expandir y colapsar spans individuales y sus hijos, por lo que esto necesita poder cambiar y hacer que la interfaz de usuario reaccione a eso. La segunda cosa que tenemos son los resultados de búsqueda, que es un conjunto de IDs de spans. Y el propósito de esto es que si el usuario escribe algún texto de búsqueda, queremos resaltar todos los spans que coinciden con su búsqueda. Finalmente, tenemos una función get span que simplemente devuelve un span particular basado en su ID de span.

Ahora este es el punto donde logramos mostrar un millón de spans a la vez, pero aún había algunas cosas que necesitaban ser mejoradas. Un problema que permanecía es que a veces si mapeábamos sobre un gran conjunto de spans, aún obteníamos errores de falta de memoria. La razón de esto es que la forma en que funciona map es que si mapeas sobre 1 millón de elementos, brevemente terminarás con 2 millones de elementos, ya que crea un segundo array para los resultados del map. Lo que eso significa para ti es que si necesitas procesar datos, recomendaría usar un bucle for y procesar manualmente cada elemento. Después de haberlo procesado, puedes eliminar del array original y esperar que el recolector de basura haga su trabajo. Para llevar eso un paso más allá, shift y unshift son cada uno O de N, pero push y pop son O de 1. Así que si estás tratando con arrays muy grandes, recomendaría apegarse a push y pop.

Así que en este punto habíamos resuelto la mayoría de los grandes problemas en torno a la memoria de cómputo, pero aún había un problema muy grande y este es en realidad el mayor problema restante en la aplicación actualmente. Este problema es obtener los datos reales. La desnormalización ayuda un poco porque obtener spans que solo tienen la información necesaria para la vista de cascada es mucho menos datos que obtener los spans completos, pero en algún punto sigue siendo una gran cantidad de datos. Ahora, si quieres entender qué son tus solicitudes de red y por qué son lentas, puedes usar la pestaña de red en Chrome DevTools y hay dos secciones allí que la gente no usa tan a menudo. La primera es la pestaña de iniciador, que encuentro genial. Si tienes mapas de origen, puedes ver qué función y qué línea realmente proviene una solicitud de red en una especie de forma de traza de pila, para que puedas averiguar dónde en tu código esta solicitud de red está realmente siendo causada.

8. Optimizing Network Performance and Payload Size

Short description:

La pestaña de timing proporciona duraciones para diferentes categorías de solicitudes. El tiempo de cola puede indicar limitaciones de concurrencia. Esperar la respuesta del servidor puede requerir optimizaciones del lado del servidor. La descarga de contenido se puede optimizar enviando una carga útil más pequeña y utilizando un formato de respuesta columnar. Aunque hay un costo de serialización en el frontend, vale la pena por la ganancia de rendimiento. Otra optimización es usar una tabla de búsqueda para los nombres de servicio en lugar de enviarlos repetidamente, pero los ahorros en el tamaño de la carga útil no fueron significativos.

Y la segunda que encuentro muy útil es la pestaña de timing. En la pestaña de timing, obtendrás las duraciones de la solicitud divididas en algunas categorías diferentes. La primera es la cola, y si pasas mucho tiempo en cola, generalmente significa que se ha alcanzado el límite de concurrencia. Generalmente se permiten seis u ocho solicitudes concurrentes, por lo que si hay más de esas en la cola, algunas de ellas tendrán que esperar. Pero la cola también puede estar relacionada con otras cosas como DNS, el navegador decidiendo que es más urgente tratar con otras cosas como HTML y CSS, esperando en el hilo principal, o cosas relacionadas con service workers. Pero generalmente si pasas mucho tiempo en cola, se debe a la concurrencia.

La segunda importante es esperar la respuesta del servidor, y todo esto significa que es la cantidad de tiempo que tu backend realmente necesita para procesar la solicitud y enviar una respuesta. Así que si esto toma mucho tiempo, deberías ver si hay algo que puedas optimizar del lado del servidor.

La última es la descarga de contenido, y aparte de obtener servidores más rápidos, la única forma en que puedes optimizar esto es enviando una carga útil más pequeña. Ahora, enviar una carga útil más pequeña no siempre es fácil. Si estás enviando datos innecesarios, entonces, por supuesto, deberías dejar de enviarlos. Pero si ya solo estás enviando los datos necesarios, entonces puede ser difícil encontrar optimizaciones. Pero aquí hay una que encontramos que ayuda bastante. Esa optimización es cambiar el formato de transmisión de pensar en los datos en términos de filas a pensar en los datos en términos de columnas.

En Axiom, no hicimos específicamente este cambio para tracing, pero también tuvo grandes impactos de rendimiento en tracing. Si implementas esto, lo que significa es que no necesitas incluir los nombres de las columnas en cada fila. Así que en el ejemplo de la izquierda, puedes ver que cada fila tiene ID y cada fila tiene service. Esto significa que en tu Stringify JSON, estás enviando ese ID y ese service decenas de miles o cientos de miles o millones de veces. Ahora se comprime con gzip hasta cierto punto, pero aun así hay un poco de sobrecarga. Y cambiar a una respuesta columnar puede ayudar mucho con eso. Además, si tu backend está en un lenguaje con un sistema de tipos más eficiente y un modelo de memoria que JavaScript, entonces los arrays de elementos de tamaño igual tienden a ser mucho más rápidos de trabajar que los structs u objetos, por lo que también puede ser beneficioso en ese sentido.

Por supuesto, si quieres mostrar un montón de filas de datos en React, entonces React generalmente prefiere tener esos datos en un formato de fila. Así que terminas pagando un costo de serialización en el frontend, pero encontramos que vale absolutamente la pena el sobrecosto de rendimiento y todo es mucho más rápido en total. Ahora, como dije antes, a la escala con la que estamos tratando aquí, que son decenas de miles o cientos de miles o incluso millones de elementos, incluso con estas optimizaciones sigue siendo una gran cantidad de datos. Así que también investigamos otra optimización. En una traza típica, incluso si hay muchos spans, generalmente son producidos solo por un pequeño número de servicios.

Así que, por ejemplo, podrías tener un servicio que representa tu API, tal vez tengas otro que representa auth y otro que representa una base de datos, etc. E incluso en el caso de diez mil spans, probablemente sea menos de mil servicios. Así que en lugar de enviar un array que tiene estos nombres de servicio una y otra vez, ¿sería más barato crear una tabla de búsqueda de los nombres de servicio y luego solo enviar el índice de la tabla de búsqueda al frontend junto con la tabla misma? Lo que encontramos es que hay ahorros menores en la carga útil que se envía de esta manera, pero simplemente no valió la pena el esfuerzo. gzip y v8 ya optimizan muy bien para valores de cadena repetidos, por lo que esto terminó simplemente no haciendo una gran diferencia.

9. Optimizing Request Handling and Performance

Short description:

Dividir los datos en partes y usar paginación basada en cursores puede optimizar el manejo de solicitudes lentas. Las APIs paginadas pueden mejorar el rendimiento al permitir reintentos más pequeños y económicos. El tamaño del fragmento depende del tipo de datos. Evita cursores basados en ID para un mejor paralelismo. Una cultura de preocupación por el rendimiento es crucial, enfocarse en los cuellos de botella y hacer pequeñas mejoras puede tener un impacto significativo. Optimiza solo donde importa para evitar perder tiempo. Asegúrate de que las optimizaciones realmente beneficien a los usuarios.

Pero teníamos otro problema. Si esta solicitud lenta para una gran cantidad de datos falla, entonces necesitamos comenzar de nuevo. Y eso no es genial. Así que lo que comenzamos a hacer en su lugar es dividir nuestros datos en partes. Lo que significa dividir en partes es que en lugar de enviar todos los resultados de una vez, los enviamos poco a poco y usamos paginación basada en cursores para saber cuál debería ser el siguiente bloque de resultados. Mencioné la paginación al principio como una forma de evitar lidiar con el problema completo del que trata esta charla, pero incluso si decides que este es un problema con el que necesitas lidiar, una API paginada aún puede ayudarte mucho.

Varias solicitudes pequeñas a menudo terminarán más rápido que una solicitud grande, y si una de las solicitudes más pequeñas falla, es mucho más económico reintentar solo esa pequeña que reintentar la grande si fallara. También hay una desventaja en esto, que es que varias solicitudes pequeñas tienden a ser más costosas del lado del servidor a menos que seas bastante bueno en el almacenamiento en caché. Pero encontramos que la diferencia hecha al dividir en partes era tan grande que no era negociable terminar dividiendo los datos en partes. Encontrar el tamaño ideal del fragmento puede ser un poco de prueba y error. Encontrar el tamaño ideal del fragmento puede ser un poco de prueba y error, y en nuestro caso encontramos que alrededor de 10,000 elementos era el tamaño correcto. Pero esto va a depender enormemente del tipo de datos con los que estés tratando.

Mi último consejo sobre dividir en partes, y actualmente no estamos haciendo esto pero desearía que lo hiciéramos, es evitar cursores basados en ID. Hay aproximadamente dos formas de paginar. La primera es decir, dame los primeros 10 resultados, bien ahora dame los resultados del 11 al 20, y así sucesivamente. Pero en algunos casos, el backend no sabe realmente dónde comienza el resultado número 11, por lo que lo que puedes hacer en su lugar es usar el ID de la última fila que se devolvió como el cursor para la siguiente solicitud. Esto soluciona el problema de que el backend sepa qué datos enviar a continuación, pero significa que necesitas esperar al primer lote para poder obtener el segundo lote, por lo que pierdes cualquier ventaja que se obtendría con el paralelismo. Ahora puede ser tentador pensar que si envías cuatro solicitudes en paralelo, obtendrás tus datos cuatro veces más rápido, pero generalmente esto no es cierto.

Al hacer algunas pruebas, encontramos que solo estamos obteniendo tal vez un 10 a 20 por ciento mejor rendimiento con esto, y la razón es que para escalar linealmente aquí, tanto la conexión de red del usuario como la API y la concurrencia del navegador necesitan escalar linealmente, y eso simplemente no suele ser cierto. Así que si puedes usar paginación basada en desplazamiento, lo recomendaría encarecidamente, pero no pierdes tanto rendimiento al no hacerlo como podrías pensar. El último punto que quiero hacer es que necesitas una cultura de preocupación por el rendimiento. Lo que esto significa es que necesitas mirar constantemente dónde están los cuellos de botella, y si hay algo que podrías hacer para minimizarlos. Hay un montón de pequeñas mejoras que no he mencionado hoy porque no serán relevantes en ningún lugar excepto en los objetivos de base de código específicos de axiom, pero el punto que quiero hacer es que hacer muchas de estas pequeñas mejoras en el camino crítico realmente se acumula. Si haces un cuello de botella un 10% más rápido 24 veces, en realidad lo has hecho 10 veces más rápido, lo cual es una gran mejora. Pero por supuesto esto se aplica solo a tus cuellos de botella, y como dije al principio, la optimización excesiva en lugares donde no es importante puede terminar siendo una gran pérdida de tiempo que no da ningún beneficio a tus usuarios.

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Una Guía del Comportamiento de Renderizado de React
React Advanced 2022React Advanced 2022
25 min
Una Guía del Comportamiento de Renderizado de React
Top Content
This transcription provides a brief guide to React rendering behavior. It explains the process of rendering, comparing new and old elements, and the importance of pure rendering without side effects. It also covers topics such as batching and double rendering, optimizing rendering and using context and Redux in React. Overall, it offers valuable insights for developers looking to understand and optimize React rendering.
Acelerando tu aplicación React con menos JavaScript
React Summit 2023React Summit 2023
32 min
Acelerando tu aplicación React con menos JavaScript
Top Content
Mishko, the creator of Angular and AngularJS, discusses the challenges of website performance and JavaScript hydration. He explains the differences between client-side and server-side rendering and introduces Quik as a solution for efficient component hydration. Mishko demonstrates examples of state management and intercommunication using Quik. He highlights the performance benefits of using Quik with React and emphasizes the importance of reducing JavaScript size for better performance. Finally, he mentions the use of QUIC in both MPA and SPA applications for improved startup performance.
Concurrencia en React, Explicada
React Summit 2023React Summit 2023
23 min
Concurrencia en React, Explicada
Top Content
React 18's concurrent rendering, specifically the useTransition hook, optimizes app performance by allowing non-urgent updates to be processed without freezing the UI. However, there are drawbacks such as longer processing time for non-urgent updates and increased CPU usage. The useTransition hook works similarly to throttling or bouncing, making it useful for addressing performance issues caused by multiple small components. Libraries like React Query may require the use of alternative APIs to handle urgent and non-urgent updates effectively.
How React Compiler Performs on Real Code
React Advanced 2024React Advanced 2024
31 min
How React Compiler Performs on Real Code
Top Content
I'm Nadia, a developer experienced in performance, re-renders, and React. The React team released the React compiler, which eliminates the need for memoization. The compiler optimizes code by automatically memoizing components, props, and hook dependencies. It shows promise in managing changing references and improving performance. Real app testing and synthetic examples have been used to evaluate its effectiveness. The impact on initial load performance is minimal, but further investigation is needed for interactions performance. The React query library simplifies data fetching and caching. The compiler has limitations and may not catch every re-render, especially with external libraries. Enabling the compiler can improve performance but manual memorization is still necessary for optimal results. There are risks of overreliance and messy code, but the compiler can be used file by file or folder by folder with thorough testing. Practice makes incredible cats. Thank you, Nadia!
Optimización de juegos HTML5: 10 años de aprendizaje
JS GameDev Summit 2022JS GameDev Summit 2022
33 min
Optimización de juegos HTML5: 10 años de aprendizaje
Top Content
PlayCanvas is an open-source game engine used by game developers worldwide. Optimization is crucial for HTML5 games, focusing on load times and frame rate. Texture and mesh optimization can significantly reduce download sizes. GLTF and GLB formats offer smaller file sizes and faster parsing times. Compressing game resources and using efficient file formats can improve load times. Framerate optimization and resolution scaling are important for better performance. Managing draw calls and using batching techniques can optimize performance. Browser DevTools, such as Chrome and Firefox, are useful for debugging and profiling. Detecting device performance and optimizing based on specific devices can improve game performance. Apple is making progress with WebGPU implementation. HTML5 games can be shipped to the App Store using Cordova.
El Futuro de las Herramientas de Rendimiento
JSNation 2022JSNation 2022
21 min
El Futuro de las Herramientas de Rendimiento
Top Content
Today's Talk discusses the future of performance tooling, focusing on user-centric, actionable, and contextual approaches. The introduction highlights Adi Osmani's expertise in performance tools and his passion for DevTools features. The Talk explores the integration of user flows into DevTools and Lighthouse, enabling performance measurement and optimization. It also showcases the import/export feature for user flows and the collaboration potential with Lighthouse. The Talk further delves into the use of flows with other tools like web page test and Cypress, offering cross-browser testing capabilities. The actionable aspect emphasizes the importance of metrics like Interaction to Next Paint and Total Blocking Time, as well as the improvements in Lighthouse and performance debugging tools. Lastly, the Talk emphasizes the iterative nature of performance improvement and the user-centric, actionable, and contextual future of performance tooling.

Workshops on related topic

Masterclass de Depuración de Rendimiento de React
React Summit 2023React Summit 2023
170 min
Masterclass de Depuración de Rendimiento de React
Top Content
Featured Workshop
Ivan Akulov
Ivan Akulov
Los primeros intentos de Ivan en la depuración de rendimiento fueron caóticos. Vería una interacción lenta, intentaría una optimización aleatoria, vería que no ayudaba, y seguiría intentando otras optimizaciones hasta que encontraba la correcta (o se rendía).
En aquel entonces, Ivan no sabía cómo usar bien las herramientas de rendimiento. Haría una grabación en Chrome DevTools o React Profiler, la examinaría, intentaría hacer clic en cosas aleatorias, y luego la cerraría frustrado unos minutos después. Ahora, Ivan sabe exactamente dónde y qué buscar. Y en esta masterclass, Ivan te enseñará eso también.
Así es como va a funcionar. Tomaremos una aplicación lenta → la depuraremos (usando herramientas como Chrome DevTools, React Profiler, y why-did-you-render) → identificaremos el cuello de botella → y luego repetiremos, varias veces más. No hablaremos de las soluciones (en el 90% de los casos, es simplemente el viejo y regular useMemo() o memo()). Pero hablaremos de todo lo que viene antes - y aprenderemos a analizar cualquier problema de rendimiento de React, paso a paso.
(Nota: Esta masterclass es más adecuada para ingenieros que ya están familiarizados con cómo funcionan useMemo() y memo() - pero quieren mejorar en el uso de las herramientas de rendimiento alrededor de React. Además, estaremos cubriendo el rendimiento de la interacción, no la velocidad de carga, por lo que no escucharás una palabra sobre Lighthouse 🤐)
Next.js 13: Estrategias de Obtención de Datos
React Day Berlin 2022React Day Berlin 2022
53 min
Next.js 13: Estrategias de Obtención de Datos
Top Content
Workshop
Alice De Mauro
Alice De Mauro
- Introducción- Prerrequisitos para la masterclass- Estrategias de obtención: fundamentos- Estrategias de obtención – práctica: API de obtención, caché (estática VS dinámica), revalidar, suspense (obtención de datos en paralelo)- Prueba tu construcción y sírvela en Vercel- Futuro: Componentes de servidor VS Componentes de cliente- Huevo de pascua de la masterclass (no relacionado con el tema, destacando la accesibilidad)- Conclusión
Depuración del Rendimiento de React
React Advanced 2023React Advanced 2023
148 min
Depuración del Rendimiento de React
Workshop
Ivan Akulov
Ivan Akulov
Los primeros intentos de Ivan en la depuración de rendimiento fueron caóticos. Veía una interacción lenta, probaba una optimización aleatoria, veía que no ayudaba, y seguía probando otras optimizaciones hasta que encontraba la correcta (o se rendía).
En aquel entonces, Ivan no sabía cómo usar bien las herramientas de rendimiento. Hacía una grabación en Chrome DevTools o React Profiler, la examinaba, intentaba hacer clic en cosas al azar, y luego la cerraba frustrado unos minutos después. Ahora, Ivan sabe exactamente dónde y qué buscar. Y en esta masterclass, Ivan te enseñará eso también.
Así es como va a funcionar. Tomaremos una aplicación lenta → la depuraremos (usando herramientas como Chrome DevTools, React Profiler, y why-did-you-render) → identificaremos el cuello de botella → y luego repetiremos, varias veces más. No hablaremos de las soluciones (en el 90% de los casos, es simplemente el viejo y regular useMemo() o memo()). Pero hablaremos de todo lo que viene antes - y aprenderemos cómo analizar cualquier problema de rendimiento de React, paso a paso.
(Nota: Esta masterclass es más adecuada para ingenieros que ya están familiarizados con cómo funcionan useMemo() y memo() - pero quieren mejorar en el uso de las herramientas de rendimiento alrededor de React. Además, cubriremos el rendimiento de interacción, no la velocidad de carga, por lo que no escucharás una palabra sobre Lighthouse 🤐)
Construyendo aplicaciones web que iluminan Internet con QwikCity
JSNation 2023JSNation 2023
170 min
Construyendo aplicaciones web que iluminan Internet con QwikCity
WorkshopFree
Miško Hevery
Miško Hevery
Construir aplicaciones web instantáneas a gran escala ha sido elusivo. Los sitios del mundo real necesitan seguimiento, análisis y interfaces y interacciones de usuario complejas. Siempre comenzamos con las mejores intenciones pero terminamos con un sitio menos que ideal.
QwikCity es un nuevo meta-framework que te permite construir aplicaciones a gran escala con un rendimiento de inicio constante. Veremos cómo construir una aplicación QwikCity y qué la hace única. El masterclass te mostrará cómo configurar un proyecto QwikCity. Cómo funciona el enrutamiento con el diseño. La aplicación de demostración obtendrá datos y los presentará al usuario en un formulario editable. Y finalmente, cómo se puede utilizar la autenticación. Todas las partes básicas para cualquier aplicación a gran escala.
En el camino, también veremos qué hace que Qwik sea único y cómo la capacidad de reanudación permite un rendimiento de inicio constante sin importar la complejidad de la aplicación.
Masterclass de alto rendimiento Next.js
React Summit 2022React Summit 2022
50 min
Masterclass de alto rendimiento Next.js
Workshop
Michele Riva
Michele Riva
Next.js es un marco convincente que facilita muchas tareas al proporcionar muchas soluciones listas para usar. Pero tan pronto como nuestra aplicación necesita escalar, es esencial mantener un alto rendimiento sin comprometer el mantenimiento y los costos del servidor. En este masterclass, veremos cómo analizar el rendimiento de Next.js, el uso de recursos, cómo escalarlo y cómo tomar las decisiones correctas al escribir la arquitectura de la aplicación.
Maximizar el rendimiento de la aplicación optimizando las fuentes web
Vue.js London 2023Vue.js London 2023
49 min
Maximizar el rendimiento de la aplicación optimizando las fuentes web
WorkshopFree
Lazar Nikolov
Lazar Nikolov
Acabas de llegar a una página web y tratas de hacer clic en un elemento en particular, pero justo antes de hacerlo, se carga un anuncio encima y terminas haciendo clic en eso en su lugar.
Eso... eso es un cambio de diseño. Todos, tanto los desarrolladores como los usuarios, saben que los cambios de diseño son malos. Y cuanto más tarde ocurran, más interrupciones causarán a los usuarios. En este masterclass vamos a analizar cómo las fuentes web causan cambios de diseño y explorar algunas estrategias para cargar fuentes web sin causar grandes cambios de diseño.
Tabla de contenidos:¿Qué es CLS y cómo se calcula?¿Cómo las fuentes pueden causar CLS?Estrategias de carga de fuentes para minimizar CLSRecapitulación y conclusión