El día que 10.000 filas casi me mandan al psicólogo (y cómo Chrome DevTools me salvó el pellejo)
¿Te ha pasado que tu app va como un tiro con 100 registros y se convierte en un ladrillo con 10.000? A mí sí. Y no, no era la base de datos. No era la red. No era el backend. Era mi código de mierda pintando filas como si no hubiera un mañana.
Hoy te cuento la historia real de cómo encontré el cuello de botella de verdad en una app web que renderizaba una lista de 10.000 filas. Spoiler: no era donde yo pensaba. Y de paso, te enseño a usar el flamegraph sin que se te derrita el cerebro.
Vamos al lío.
El caso: la lista que se comía el navegador
Contexto rápido. Una app interna de gestión. Un panel con una tabla de usuarios, pedidos, lo que sea. El caso es que un día el equipo de soporte sube un CSV con 10.000 filas y la app… se muere.
No exagero. El navegador se queda congelado unos 8 segundos. La pestaña se pone en "no responde". El usuario hace clic tres veces pensando que no se registró, y cuando por fin reacciona, tiene tres modales abiertos. Un desastre.
Mi primer instinto fue el clásico: "será la API, que devuelve mucho JSON". Medí la red. La respuesta llegaba en 400 ms. No era eso.
Segundo sospechoso: "será el framework, que es lento". Cambiar de framework es la solución favorita de quien no quiere leer un perfil. No caí en esa trampa. Bueno, casi.
Tercer intento: abrí el perfil de rendimiento. Y ahí empezó la fiesta.
Paso 1: Grabar sin engañarte a ti mismo
Antes de tocar nada, hay que medir. Y medir bien.
Abrí la pestaña Performance del navegador, activé la grabación y reproduje el escenario: cargar las 10.000 filas y esperar. Nada de "voy a optimizar por intuición". Eso es como ir al médico y autodiagnosticarte con Dr. Google.
Consejo de amiga: haz la grabación en una ventana de incógnito, sin extensiones. Tengo un compañero que una vez culpó al código durante dos días y resultó ser una extensión que inyectaba un observer en cada nodo del DOM. Dos días. De su vida. Que no volverán.
Graba, espera a que la lista se pinte, y para. Ya tienes material.
Paso 2: Leer el flamegraph sin entrar en pánico
Cuando abres el perfil por primera vez, parece un cuadro de Jackson Pollock. Barritas de colores, nombres raros, cosas que se solapan. Respira.
El flamegraph es básicamente una foto de quién hizo qué y durante cuánto tiempo. Cada barra es una función. Cuanto más ancha, más tiempo consumió. Cuanto más abajo, más profunda en la pila de llamadas. Si ves una barra ancha y plana en la parte de arriba, ahí está el problema.
En mi caso, vi algo que me heló la sangre: una barra gigante llamada layout que ocupaba casi la mitad del tiempo total. Y debajo, un montón de llamadas a recalculate style repitiéndose como un disco rayado.
Traducción: el navegador estaba recalculando estilos y recolocando elementos una y otra vez. No era el JavaScript en sí. Era el navegador sufriendo porque yo le obligaba a repintar la tabla entera cada vez que añadía una fila.
Paso 3: El culpable real (y no, no era el framework)
Aquí viene la lección. Yo estaba convencida de que el problema era "renderizar 10.000 nodos". Y sí, en parte. Pero el cuello de botella real era más sutil:
Cada fila se añadía al DOM individualmente, dentro de un bucle, y cada inserción forzaba al navegador a recalcular el layout de toda la tabla.
Es el equivalente a pintar una pared ladrillo a ladrillo y, después de cada ladrillo, dar un paso atrás para comprobar que la pared sigue recta. Técnicamente funciona. Prácticamente, eres un psicópata.
El navegador no se ahogaba por tener 10.000 filas. Se ahogaba por hacer 10.000 recálculos de layout. Cada uno pequeño, pero sumados: 8 segundos de congelación.
Paso 4: Las tres balas que mataron al problema
Con el diagnóstico claro, ataqué en tres frentes. Por orden de impacto:
- Fragmentar el DOM antes de insertar. En lugar de añadir cada fila al contenedor, las construí en un fragmento en memoria y lo inserté de una sola vez. Un solo recálculo de layout. Boom. De 8 segundos a 2.
- Virtualizar la lista. Aquí está la clave de verdad. ¿Por qué renderizar 10.000 filas si el usuario solo ve 20 en pantalla? Solo pinto las visibles y reciclo nodos al hacer scroll. De 2 segundos a 80 ms. Sí, has leído bien.
requestAnimationFramepara las actualizaciones. Cuando hay cambios frecuentes, dejo que el navegador decida cuándo pintar. No le metas prisa. Él sabe mejor que tú cuándo tiene un hueco libre.
Resultado final: la lista carga en menos de 100 ms. El usuario ni se entera. Y yo recuperé mi salud mental.
Paso 5: Verificar que no te has autoengañado
Optimizar sin volver a medir es como hacer dieta sin pesarte. Puedes creer que vas bien y estar igual o peor.
Volví a grabar el perfil. La barra de layout había desaparecido casi por completo. El flamegraph ahora era aburrido: barritas cortas, todo repartido, sin picos sospechosos. Un flamegraph aburrido es un flamegraph sano. Grábatelo.
Y ojo con un detalle: verifica también en un móvil de gama media o con la CPU throttled. En mi portátil iba genial, pero en un Android barato aún se notaba. Bajé el throttling a 4x en DevTools y ajusté un par de cosas más. Nunca optimices solo para tu máquina. Tu máquina es una mentirosa.
Lo que aprendí (y lo que deberías aprender tú)
- El cuello de botella casi nunca está donde crees. Yo iba a por el framework y me encontré con el layout del navegador. Mide antes de tocar.
- El flamegraph no miente. Las intuiciones sí. Cuando tengas dudas, graba, mira barras anchas, y sigue el rastro hacia abajo. El culpable siempre deja huella.
- Renderizar no es el problema; renderizar mal, sí. 10.000 filas no son muchas si solo pintas las que se ven. Son muchísimas si pintas todas y encima lo haces una a una.
- El rendimiento no es una feature que se añade al final. Es una decisión que tomas en cada línea. La lista que hoy va rápida con 100 filas puede ser el infierno de mañana con 10.000. Y tú no vas a estar ahí para verlo.
Así que ya sabes. La próxima vez que tu app se arrastre, no toques nada. Abre la pestaña Performance, graba, y deja que el flamegraph te confiese quién es el asesino.
¿Y tú? ¿Cuál ha sido el cuello de botella más absurdo que te has encontrado? Porque apuesto a que no era el que pensabas al principio.
Comentarios
Deja un comentario