Más allá del tamaño del paquete: descubriendo qué es lo que realmente ralentiza tu aplicación web
Por qué raspar kilobytes rara vez soluciona un aplicación lenta, y cómo rastrear el tiempo real de espera a través de servidores, procesos en cascada, mecanismos de hidratación, scripts de terceros e imágenes.
Una escena familiar se repite en los equipos de frontend: se pasan semanas intentando reducir 40 KB de un paquete JavaScript, mientras que una consulta a la base de datos que tarda 900 milisegundos permanece sin modificar en el camino crítico de las solicitudes. Se cambia una biblioteca de iconos, se reemplaza una dependencia, se configura otro plugin de empaquetado, y surge un debate sobre si un paquete pesa 18 KB o 12 KB. Luego alguien carga la aplicación en un teléfono real a través de una red real, y sigue pareciendo lenta.
Este artículo explica por qué el tamaño del paquete suele convertirse en el objetivo equivocado, de dónde proviene realmente la demora y cómo ejecutar un ciclo de optimización que resuelva primero los problemas más graves. Los paquetes más pequeños sí ayudan, a veces mucho. Pero si la página es lenta porque espera en el servidor, bloquea el renderizado, realiza tareas innecesarias, envía demasiadas solicitudes o carga un árbol de componentes enorme, reducir otros 20 KB no la salvará.
Por qué el tamaño del paquete se convirtió en la meta de rendimiento por defecto
El tamaño del paquete es atractivo porque se trata de un número. Tu proceso de compilación muestra algo similar a esto:
main.js 842 KB
vendor.js 611 KB
styles.css 94 KB
Alguien propone que el JavaScript no supere los 500 KB, y de repente el equipo tiene un objetivo. Se puede hacer cumplir en CI, seguir su evolución a través de las solicitudes de integración y celebrar cada reducción. Eso parece progreso en ingeniería, y a veces realmente lo es.
Los problemas comienzan cuando el número deja de ser un síntoma y se convierte en el objetivo. Los equipos tienden a optimizar la parte del rendimiento que pueden ver con mayor claridad, no aquella que le cuesta más tiempo a los usuarios. Para entender por qué eso es importante, comparemos dos aplicaciones hipotéticas.
Aplicación A: paquete pequeño, todo lo demás lento
La primera aplicación incluye un paquete ligero:
JavaScript: 250 KB
Pero todo lo demás relacionado con ella es costoso:
Server response: 1.2s
Database query: 700ms
API calls before rendering: 5
Main-thread work: 900ms
Aplicación B: paquete grande, ruta rápida al contenido
La segunda aplicación incluye casi tres veces más JavaScript:
JavaScript: 700 KB
Pero tanto su servidor como su cliente realizan mucho menos trabajo antes de que la página sea útil:
Server response: 150ms
Database query: 40ms
API calls before rendering: 1
Main-thread work: 180ms
La Aplicación A no gana automáticamente. Una aplicación de tipo B suele parecer más rápida, ya que tarda aproximadamente un segundo menos en el servidor, en la base de datos, en solicitudes secuenciales y en tareas del hilo principal, lo cual eclipsa la descarga adicional para la mayoría de los usuarios con una conexión razonable. El principio en el que debe basarse cualquier discusión sobre rendimiento es simple: los usuarios no perciben kilobytes, sino el tiempo de espera.
La carga de la página es un proceso largo, no tres pasos
Muchos desarrolladores tienen en mente un modelo simplificado del proceso de carga:
Download JavaScript
↓
Execute JavaScript
↓
Page appears
Una solicitud real pasa por muchas más etapas, cada una de las cuales puede causar retrasos:
DNS
↓
Connection
↓
TLS
↓
Request
↓
Server processing
↓
Database
↓
Response
↓
HTML parsing
↓
CSS processing
↓
JavaScript download
↓
JavaScript parsing
↓
JavaScript execution
↓
Hydration
↓
API requests
↓
Rendering
↓
Layout
↓
Paint
La búsqueda DNS, la configuración de la conexión y la negociación TLS ocurren antes de que el servidor vea algo. Luego vienen el procesamiento del servidor y las operaciones en la base de datos. A continuación, el navegador analiza el HTML, procesa el CSS, descarga, analiza y ejecuta JavaScript, carga los datos dinámicamente, envía solicitudes a la API, y solo entonces renderiza, diseña y dibuja el contenido. Y cuando el usuario hace clic, gran parte de este ciclo se repite.
Con tantas etapas, el paquete de recursos es simplemente un lugar donde el tiempo puede desaparecer. “Hacer que el paquete sea más pequeño” es un primer paso deficiente, ya que asume la solución antes de saber dónde se está perdiendo el tiempo.
El servidor podría ser la parte más lenta de tu frontend
Los ingenieros de frontend suelen considerar el rendimiento como un problema del navegador. Abren las Herramientas de Desarrollo, inspeccionan la pestaña Network y los fragmentos de JavaScript, y ejecutan Lighthouse. Pero una gran parte de la velocidad percibida en el frontend se determina antes incluso de que el navegador reciba algo útil.
Tomemos una solicitud al panel de control:
GET /dashboard
Detrás de esto, el servidor podría hacer todo esto antes de responder:
→ authenticate user
→ fetch organization
→ fetch permissions
→ query projects
→ query project statistics
→ query notifications
→ calculate recommendations
→ render response
Si esa cadena de operaciones tarda 1.4 segundos, reducir el tamaño del paquete de 600 KB a 500 KB apenas cambia la experiencia, ya que los primeros bytes significativos siguen llegando a los 1.4 segundos. El navegador no puede renderizar datos que aún no ha recibido.
Un culpable frecuente es el código de backend que espera que se realicen operaciones independientes una tras otra. Comienza con la obtención de información del usuario:
const user = await getUser();
Y continúa con una cadena de esperas adicionales para la organización, los proyectos y las notificaciones, cada una esperando a que termine la anterior:
const organization = await getOrganization(user.orgId);const projects = await getProjects(organization.id);const notifications = await getNotifications(user.id);
Algunos de estos pasos realmente son dependientes: la búsqueda de la organización necesita user.orgId, y los proyectos necesitan el ID de la organización. Pero todo aquello que no depende de un resultado anterior está pagando su latencia en serie sin motivo alguno. Cuando las operaciones son independientes, iniciarlas juntas y esperarlas conjuntamente puede reducir drásticamente el tiempo de respuesta:
const [user, notifications] = await Promise.all([
getUser(),
getNotifications()
]);
Observe que la versión paralela llama a getNotifications() sin un ID de usuario. Eso solo funciona si las notificaciones pueden obtenerse a partir de algo ya disponible, como la sesión; de lo contrario, esa llamada aún debe esperar al usuario. La regla general es mapear el verdadero grafo de dependencias y ejecutar cada nivel de él de forma concurrente. Para más información sobre cómo elegir entre estos patrones, consulte nuestra guía sobre Promise.all, Promise.race y awaits secuenciales. Un cambio como este puede superar con facilidad el rendimiento de cualquier tarea de empaquetado.
Los enfoques en cascada cuestan más que bytes
Uno de los lugares más productivos para comenzar una investigación es la pestaña Red, no el analizador de paquetes. Un patrón muy común se ve así:
HTML
↓
JavaScript
↓
API A
↓
API B
↓
API C
↓
API D
Cada paso espera al anterior, y cada transmisión añade un viaje de ida y vuelta que genera latencia. Compárelo con un diseño en el que la primera respuesta ya incluye todo lo que necesita la página:
HTML
↓
API response containing everything required
La segunda versión puede transferir más bytes y, aun así, ser drásticamente más rápida. Los bytes y la latencia son problemas separados. Una respuesta de 100 KB que llega de inmediato puede superar a una de 20 KB que necesita cuatro viajes secuenciales antes de que la página pueda hacer algo útil, especialmente en redes móviles donde cada viaje es costoso.
Por lo tanto, cuando observe que un endpoint devuelve 300 KB, su primer impulso será reducir el volumen de los datos. Eso puede ser útil, pero una mejor pregunta inicial es por qué el usuario necesita esa respuesta antes de poder interactuar con la página. La respuesta suele revelar tareas que se pueden posponer o eliminar por completo.
El rendimiento de JavaScript es más importante que su tamaño
Otra trampa relacionada es confundir el tamaño del código JavaScript con la labor que realiza. Un paquete de 500 KB no es necesariamente un desastre. Lo que realmente importa es lo que el navegador debe hacer con él:
- Descargarlo.
- Analizarlo.
- Compilarlo.
- Ejecutarlo.
- Crear el estado de la aplicación.
- Construir los árboles de componentes.
- Asociar manejadores de eventos.
- Actualizar el marcado generado por el servidor.
- Volver a calcular el diseño.
- Dibujar el resultado final.
Dos aplicaciones con tamaños de paquete similares pueden diferir enormemente en cuanto al costo de ejecución. Considere una tabla con 5,000 filas: rara vez el problema radica en los datos en sí; lo habitual es que sea la renderización de los nodos DOM correspondientes a esas 5,000 filas interactivas. La solución no consiste en reducir 50 KB de código, sino en renderizar solo las unas 30 filas que se muestran actualmente. Esa técnica, la virtualización, mantiene la misma aplicación y los mismos datos al tiempo que elimina en gran medida el trabajo del navegador.
Cuando la hidratación se convierte en el cuello de botella
Esta distinción es especialmente evidente en React y otros frameworks de componentes que realizan la renderización en el servidor. La renderización en servidor permite mostrar rápidamente el HTML en pantalla, pero el navegador podría necesitar luego hidratar un árbol de componentes muy grande antes de que algo responda a las entradas del usuario:
HTML arrives quickly
↓
User sees content
↓
Browser starts hydration
↓
Large amount of JavaScript executes
↓
Page becomes interactive
La página parece lista mucho antes de estarlo realmente. Por eso, medir únicamente cuando el contenido aparece por primera vez puede engañar. Un panel de control con 200 componentes interactivos puede generar HTML perfectamente válido y, aun así, consumir una cantidad significativa de recursos del procesador durante el proceso de hidratación, lo que hace que los clics queden sin respuesta.
La pregunta relevante aquí no es si el paquete es demasiado grande, sino por qué tanto código debe volverse interactivo de inmediato. Algunos componentes podrían no necesitar JavaScript en el cliente en absoluto. Algunas interacciones pueden aislarse en secciones pequeñas. Algunos widgets pueden cargarse más tarde, y algunos componentes generados por el servidor podrían no necesitar nunca el proceso de hidratación. Técnicas como estas, que analizamos en nuestra reseña sobre el preprocesamiento parcial y el procesamiento concurrente, ofrecen resultados mucho mejores que discutir sobre una dependencia de 30 KB.
Los scripts de terceros suelen superar en importancia al propio código
Antes de lanzar una campaña de gran tamaño, verifica cuánto del código que se envía fue escrito por otra persona. Ejemplos típicos:
- análisis
- widgets de chat
- mapas de calor
- pruebas A/B
- publicidad
- Herramientas de soporte al cliente
- Grabación de sesiones
- Incrustaciones en redes sociales
- Píxeles de marketing
- Gestión de consentimientos
Cada uno de ellos puede añadir solicitudes, ejecución de scripts, tareas de diseño y actividad en la red. Irónicamente, estos scripts a menudo no pasan por ninguna revisión mientras los ingenieros invierten horas ajustando el código de la aplicación. Una página podría cargar un conjunto como este:
app.js
analytics.js
chat.js
tracking.js
experimentation.js
heatmap.js
El equipo celebra una reducción de 70 KB en app.js, aunque la página sigue ejecutando cientos de kilobytes de código de terceros. Por eso los presupuestos de rendimiento necesitan un alcance más amplio. En lugar de preguntarse cuán grande es el paquete, hay que preguntarse cuánto código debe procesar el dispositivo del usuario antes de que esta página sea útil. Las dos preguntas tienen respuestas muy diferentes.
Las imágenes pueden superar con creces todo el presupuesto destinado al JavaScript
Las imágenes de tamaño excesivo son otro punto ciego. Una sola imagen principal puede superar en importancia a todo un bloque de JavaScript optimizado:
main.js 180 KB
hero.webp 1.4 MB
product.jpg 900 KB
background.png 2.1 MB
Si se aprueba una solicitud de integración que elimina una dependencia de 12 KB mientras que una imagen de fondo de 2.1 MB se envía sin cambios, el equipo está realizando una especie de teatro de priorización en lugar de realizar optimizaciones reales. Las imágenes merecen el mismo rigor que el código:
- Preferir formatos modernos como WebP o AVIF cuando estén soportados y sean adecuados.
Ahorrar 15 KB de JavaScript significa poco si un teléfono sigue descargando 2 MB para una imagen que el usuario apenas nota.
Muchos problemas de rendimiento son problemas de arquitectura
Cuanto más profundiza, más claro se vuelve que muchos problemas de rendimiento no tienen nada que ver con ajustar el código. Proviene de la forma en que está estructurada la aplicación. Imagine una página de producto que necesita todo esto:
Product
Reviews
Recommendations
Inventory
Shipping estimate
User preferences
Related products
Si cada elemento se carga por separado después de que la página se haya cargado, es posible optimizar cada solicitud y aun así obtener una página lenta. La mejor estrategia es decidir qué debe ver el usuario primero. La respuesta inicial podría incluir únicamente:
Product
Price
Availability
Primary image
Las reseñas, recomendaciones y productos relacionados pueden cargarse posteriormente. En ese momento ya no se está optimizando la implementación; se está redefiniendo qué significa “listo” para la página, y es allí donde a menudo se logran las mayores mejoras.
Mida los hitos que realmente perciben los usuarios
Un trabajo serio en rendimiento sustituye la pregunta “¿qué tan grande es el paquete?” por “¿cuándo puede el usuario hacer algo útil?”. Esa pregunta conduce a métricas más adecuadas.
Tiempo hasta el primer contenido útil
¿Cuándo ve el usuario lo que vino a buscar? Esto suele ser específico de su producto: el saldo de la cuenta, los resultados de búsqueda, la foto del producto.
Tiempo hasta la interacción
¿Cuándo puede el usuario interactuar de manera fiable, sin que los clics se vean afectados por tareas en curso? Las versiones recientes de Lighthouse ya no incluyen el TTI en su puntuación, pero la pregunta subyacente sigue siendo útil para monitorear sus propias páginas.
Pintado del contenido más grande
¿Cuándo finaliza la renderización del contenido principal visible?
Interacción hasta el siguiente pintado
¿Con qué rapidez responde visualmente la interfaz después de que el usuario interactúa?
Cambio acumulativo de layout
¿El layout cambia constantemente mientras el usuario intenta leer o hacer clic?
Tiempo total de bloqueo
¿Cuánto tiempo está bloqueada la hilera principal por tareas que impiden que el navegador responda a las entradas?
Ninguno de estos elementos por sí solo cuenta toda la historia, pero juntos describen la experiencia mucho mejor que una sola línea como esta:
bundle.js = 487 KB
Una figura de paquete describe un recurso. Las métricas de rendimiento describen lo que experimenta el usuario.
Un bucle de optimización en el que se puede confiar
Cuando una aplicación lenta necesita reparación, eliminar dependencias no debería ser el primer paso. Un bucle bien estructurado funciona mejor.
1. Reproducir en condiciones realistas
Pruebe en dispositivos y redes representativos, no solo en una laptop rápida conectada a Wi-Fi de la oficina. Muchos de sus usuarios no cuentan con ninguno de estos.
2. Medir para encontrar el punto lento
Determine dónde se pierde el tiempo. ¿Es uno de estos los problemas principales?
server response?
network?
rendering?
JavaScript execution?
layout?
images?
third-party scripts?
3. Identificar el costo más importante
Resista la tentación de arreglar cinco cosas a la vez. Encuentre el factor que más contribuye al problema.
4. Cambiar una cosa
Haga el cambio arquitectónico o de implementación más pequeño que resuelva ese cuello de botella, para poder atribuir el resultado.
5. Mida nuevamente
Si la mejora no se refleja en las cifras, no asuma que funcionó.
6. Consolide los beneficios con una verificación de regresión
Las mejoras se desvanecen rápidamente. Alguien agrega una dependencia, un equipo de producto incluye un widget, un componente deja de ser eficiente, una consulta pasa a ser secuencial, y tres meses después vuelve a donde empezó. El rendimiento necesita medidas de protección automatizadas en los procesos CI y en la supervisión, no solo limpiezas esporádicas.
Tamaño del paquete sigue siendo importante, en su lugar adecuado
Nada de esto hace que el tamaño del paquete sea irrelevante. Los paquetes grandes aumentan los costos de descarga, análisis, compilación y ejecución, y la penalización es más severa en dispositivos y redes lentas. El dividir el código, eliminar componentes innecesarios, cargar contenido de forma diferida y recortar dependencias no utilizadas son estrategias valiosas. Lo importante es aplicarlas cuando la evidencia indica que representan el mayor problema.
Una revisión de rendimiento adecuada, ordenada por su impacto, podría ser la siguiente: primero, una respuesta lenta del servidor:
Problem:
900ms server response
Se solucionó ejecutando las solicitudes al backend en paralelo:
Action:
parallelize backend requestsResult:
-420ms
Luego, la carga costosa del panel de control:
Problem:
large dashboard hydration
Se abordó posponiendo la carga de componentes que no necesitan ser interactivos de inmediato:
Action:
defer non-critical interactive componentsResult:
-280ms main-thread work
Después, una imagen principal de tamaño excesivo:
Problem:
hero image is 1.8 MB
Se resolvió con una entrega adaptativa en formatos modernos:
Action:
responsive WebP/AVIF deliveryResult:
-1.2 MB transferred
Solo después de todo eso es cuando una dependencia pesada de JavaScript llega al principio de la lista:
Problem:
large JavaScript dependency
Sustituirla sigue permitiendo ahorrar una cantidad significativa:
Action:
replace dependencyResult:
-60 KB
Esa solución sigue siendo una buena labor. Simplemente debería estar en cuarto lugar en la cola, no en primer lugar.
Puntos clave
- Optimice por el tiempo de espera, no por el tamaño más pequeño posible del archivo. El tamaño del paquete es una de las muchas señales a considerar.
- Analice el servidor y la secuencia de solicitudes antes de usar un analizador de paquetes; la latencia y los viajes de ida y vuelta suelen costar más que los bytes.
- Mida el JavaScript según el trabajo que genera: análisis, ejecución, renderizado e integración con el DOM, y no solo su tamaño.
- Haga una auditoría de los scripts e imágenes de terceros con el mismo rigor que aplicaría a su propio código.
- Redefina qué significa “listo” para cada página para que el contenido crítico llegue primero y todo lo demás siga después.
Lecturas relacionadas
- Qué Hace que los Desarrolladores Frontend sean Valiosos en la Era de la IA — Explica por qué el entendimiento, el juicio y el pensamiento a nivel de sistema son ahora más importantes que la fluidez con los frameworks, a medida que la IA asume tareas rutinarias de programación frontend.
- Más Allá del P95: Medir la Latencia que Realmente Experimentan tus Usuarios — Por qué un P95 saludable puede coexistir con un producto lento, cómo el tiempo de espera en colas y la dispersión de solicitudes se ocultan en los paneles de control, y cómo el cronometraje por paso pone fin a las acusaciones sobre la latencia.
- Qué descarta, convierte y se niega a serializar JSON.stringify en silencio — Aprenda qué valores de JavaScript omite o modifica JSON.stringify, cómo toJSON, los reemplazadores y los restauradores lo solucionan, y cuándo structuredClone es la herramienta más adecuada.
- Dónde se escribe una función determina lo que ve: el ámbito léxico de JavaScript — Entienda cómo JavaScript resuelve un nombre de variable a través de los entornos léxicos, por qué el lugar de llamada nunca es relevante para la búsqueda, y cómo esto afecta a los manejadores de React.
- Qué optimiza el compilador de React y qué deja para usted — Entienda qué tareas relacionadas con el rendimiento automatiza el compilador de React, por qué las APIs lentas y los paquetes pesados siguen siendo su responsabilidad, y cómo adoptarlo de forma segura en una base de código React existente.
- Desmitificando la diferenciación del Virtual DOM: qué compara React y por qué vale la pena — Entienda qué es realmente el Virtual DOM de React, cómo reconcilia las diferencias entre dos árboles de elementos, qué cambios ocurren en la fase de confirmación y de dónde proviene el beneficio en términos de rendimiento.
- Reflow, Repaint, Composite: Cuánto cuesta al navegador cada cambio en CSS — Sigue el proceso de HTML y CSS a través del DOM, CSSOM, el diseño, la pintura y la composición, y aprende por qué los cambios de ancho cuestan más que los cambios de color y cómo evitar problemas en el diseño.