Optimización de inicio de JavaScript

A medida que creamos sitios que dependen más de JavaScript, a veces pagamos por lo que enviamos de formas que no siempre podemos ver fácilmente. En este artículo, explicaremos por qué un poco de disciplina puede ser útil si deseas que tu sitio se cargue y sea interactivo rápidamente en dispositivos móviles. La entrega de menos JavaScript puede significar menos tiempo en la transmisión de red, menos tiempo dedicado a descomprimir el código y menos tiempo para analizar y compilar este JavaScript.

Red

Cuando la mayoría de los desarrolladores piensan en el costo de JavaScript, lo hacen en términos del costo de descarga y ejecución. Enviar más bytes de JavaScript a través de la red tarda más a medida que la conexión del usuario es más lenta.

Cuando un navegador solicita un
recurso, se debe recuperar y, luego, descomprimir. En el caso de recursos como JavaScript, se deben analizar y compilar antes de la ejecución.

Esto puede ser un problema, ya que el tipo de conexión de red efectiva que tiene un usuario podría no ser 3G, 4G o Wi-Fi. Puedes estar conectado a la red Wi-Fi de una cafetería, pero a un hotspot celular con velocidades 2G.

Puedes reducir el costo de transferencia de red de JavaScript de las siguientes maneras:

  • Enviar solo el código que necesita el usuario
  • Reducción
  • Compresión
    • Como mínimo, usa gzip para comprimir los recursos basados en texto.
    • Considera usar Brotli ~q11. Brotli tiene un mejor rendimiento que gzip en la proporción de compresión. Ayudó a CertSimple a ahorrar un 17% en el tamaño de los bytes de JS comprimidos y a LinkedIn a ahorrar un 4% en los tiempos de carga.
  • Quita el código sin usar.
  • Almacenamiento en caché del código para minimizar los viajes de red
    • Usa el almacenamiento en caché HTTP para asegurarte de que los navegadores almacenen en caché las respuestas de forma eficaz. Determina las duraciones óptimas para las secuencias de comandos (max-age) y proporciona tokens de validación (ETag) para evitar transferir bytes sin cambios.
    • La caché de Service Worker puede hacer que la red de tu app sea resiliente y te brinde acceso anticipado a funciones como la caché de código de V8.
    • Usa el almacenamiento en caché a largo plazo para evitar tener que volver a recuperar recursos que no cambiaron. Si usas Webpack, consulta hash de nombres de archivo.

Analizar/compilar

Una vez descargado, uno de los costos más pesados de JavaScript es el tiempo que tarda un motor de JS en analizar o compilar este código. En las Herramientas para desarrolladores de Chrome, el análisis y la compilación forman parte del tiempo amarillo de "Escritura de secuencias de comandos" en el panel Rendimiento.

ALT_TEXT_HERE

En las pestañas Bottom-Up y Call Tree, se muestran los tiempos exactos de análisis y compilación:

ALT_TEXT_HERE
Panel de rendimiento de las Herramientas para desarrolladores de Chrome > De abajo hacia arriba. Con las estadísticas de llamadas del entorno de ejecución de V8 habilitadas, podemos ver el tiempo dedicado a fases como el análisis y la compilación.

Pero, ¿por qué es importante?

ALT_TEXT_HERE

Pasar mucho tiempo analizando o compilando código puede retrasar en gran medida la rapidez con la que un usuario puede interactuar con tu sitio. Cuanto más JavaScript envíes, más tiempo llevará analizarlo y compilarlo antes de que tu sitio sea interactivo.

Byte por byte, JavaScript es más costoso para que el navegador lo procese que la imagen o la fuente web de tamaño equivalente: Tom Dale

En comparación con JavaScript, el procesamiento de imágenes de tamaño equivalente implica muchos costos (de todas formas, deben decodificarse), pero, en el hardware promedio para dispositivos móviles, es más probable que JS afecte negativamente la interactividad de una página.

ALT_TEXT_HERE
Los bytes de JavaScript y de imagen tienen costos muy diferentes. Por lo general, las imágenes no bloquean el subproceso principal ni impiden que las interfaces se vuelvan interactivas mientras se decodifican y rasterizan. Sin embargo, JS puede retrasar la interactividad debido a los costos de análisis, compilación y ejecución.

Cuando hablamos de que el análisis y la compilación son lentos, el contexto es importante: estamos hablando de teléfonos celulares promedio. Los usuarios promedio pueden tener teléfonos con CPUs y GPUs lentas, sin caché de L2/L3 y que incluso pueden tener limitaciones de memoria.

Las capacidades de la red y las del dispositivo no siempre coinciden. Un usuario con una conexión de Fiber increíble no tiene necesariamente la mejor CPU para analizar y evaluar el código JavaScript que se envía a su dispositivo. Esto también es cierto en el caso contrario: una conexión de red terrible, pero una CPU muy rápida. — Kristofer Baxter, LinkedIn

A continuación, podemos ver el costo de analizar alrededor de 1 MB de JavaScript descomprimido (simple) en hardware de gama baja y alta. Existe una diferencia de entre 2 y 5 veces en el tiempo de análisis o compilación de código entre los teléfonos más rápidos del mercado y los teléfonos promedio.

ALT_TEXT_HERE
Este gráfico destaca los tiempos de análisis de un paquete de 1 MB de JavaScript (~250 KB comprimidos con gzip) en computadoras de escritorio y dispositivos móviles de diferentes clases. Cuando se analiza el costo de la comprensión, se deben tener en cuenta las cifras descomprimidas, p. ej., ~250 KB de JS comprimidos en gzip se descomprimen a ~1 MB de código.

¿Qué sucede con un sitio del mundo real, como CNN.com?

En el iPhone 8 de alta gama, solo se necesitan alrededor de 4 segundos para analizar o compilar el código JS de CNN en comparación con los alrededor de 13 segundos de un teléfono promedio (Moto G4). Esto puede afectar significativamente la rapidez con la que un usuario puede interactuar por completo con este sitio.

ALT_TEXT_HERE
En la parte superior, se muestran los tiempos de análisis que comparan el rendimiento del chip A11 Bionic de Apple con el Snapdragon 617 en un hardware de Android más promedio.

Esto destaca la importancia de realizar pruebas en hardware promedio (como el Moto G4) en lugar de solo en el teléfono que podrías tener en el bolsillo. Sin embargo, el contexto es importante: optimiza para las condiciones del dispositivo y la red que tienen tus usuarios.

ALT_TEXT_HERE
Google Analytics puede proporcionar estadísticas sobre las clases de dispositivos móviles con las que tus usuarios reales acceden a tu sitio. Esto puede brindar oportunidades para comprender las restricciones reales de la CPU o la GPU con las que operan.

¿Realmente estamos enviando demasiado JavaScript? Err, posiblemente :)

Si usamos HTTP Archive (los 500,000 sitios principales) para analizar el estado de JavaScript en dispositivos móviles, podemos ver que el 50% de los sitios tardan más de 14 segundos en ser interactivos. Estos sitios dedican hasta 4 segundos solo a analizar y compilar JS.

ALT_TEXT_HERE

Ten en cuenta el tiempo que se tarda en recuperar y procesar JS y otros recursos, y quizás no te sorprenda que los usuarios puedan esperar un tiempo antes de sentir que las páginas están listas para usarse. Definitivamente, podemos hacerlo mejor.

Quitar el código JavaScript no esencial de tus páginas puede reducir los tiempos de transmisión, el análisis y la compilación intensivos en la CPU, y la posible sobrecarga de memoria. Esto también ayuda a que tus páginas sean interactivas más rápido.

Tiempo de ejecución

No solo el análisis y la compilación pueden tener un costo. La ejecución de JavaScript (ejecutar código una vez analizado o compilado) es una de las operaciones que debe ocurrir en el subproceso principal. Los tiempos de ejecución largos también pueden retrasar la rapidez con la que un usuario puede interactuar con tu sitio.

ALT_TEXT_HERE

Si la secuencia de comandos se ejecuta durante más de 50 ms, el tiempo de interacción se retrasa por el tiempo completo que tarda en descargarse, compilarse y ejecutarse el JS. Alex Russell

Para abordar este problema, JavaScript se beneficia de estar en fragmentos pequeños para evitar bloquear el subproceso principal. Explora si puedes reducir la cantidad de trabajo que se realiza durante la ejecución.

Costos adicionales

JavaScript puede afectar el rendimiento de la página de otras maneras:

  • Memoria. Las páginas pueden parecer que se detienen o se pausan con frecuencia debido a la GC (recolección de elementos no utilizados). Cuando un navegador recupera memoria, se pausa la ejecución de JS, de modo que un navegador que recopile elementos no utilizados con frecuencia puede pausar la ejecución con más frecuencia de lo que nos gustaría. Evita las fugas de memoria y las pausas frecuentes de GC para que las páginas no tengan bloqueos.
  • Durante el tiempo de ejecución, el código JavaScript de larga duración puede bloquear el subproceso principal, lo que genera páginas que no responden. Dividir el trabajo en partes más pequeñas (con requestAnimationFrame() o requestIdleCallback() para la programación) puede minimizar los problemas de capacidad de respuesta, lo que puede ayudar a mejorar la Interaction to Next Paint (INP).

Patrones para reducir el costo de publicación de JavaScript

Cuando intentas mantener lentos los tiempos de análisis, compilación y transmisión de red para JavaScript, hay patrones que pueden ayudar, como el fragmentación basada en rutas o PRPL.

PRPL

PRPL (envío, renderización, almacenamiento en caché previo y carga diferida) es un patrón que optimiza la interactividad a través de la división y el almacenamiento en caché agresivos del código:

ALT_TEXT_HERE

Veamos el impacto que puede tener.

Analizamos el tiempo de carga de sitios móviles populares y aplicaciones web progresivas con las estadísticas de llamadas del entorno de ejecución de V8. Como podemos ver, el tiempo de análisis (que se muestra en naranja) es una parte significativa del tiempo que muchos de estos sitios dedican a lo siguiente:

ALT_TEXT_HERE

Wego, un sitio que usa PRPL, logra mantener un tiempo de análisis bajo para sus rutas y se vuelve interactivo con mucha rapidez. Muchos de los otros sitios anteriores adoptaron la división de código y los presupuestos de rendimiento para intentar reducir sus costos de JS.

Inicio progresivo

Muchos sitios optimizan la visibilidad del contenido a costa de la interactividad. Para obtener un primer procesamiento de imagen rápido cuando tienes paquetes de JavaScript grandes, los desarrolladores a veces emplean el procesamiento en el servidor y, luego, lo "actualizan" para adjuntar controladores de eventos cuando se recupera el JavaScript por fin.

Ten cuidado, ya que esto tiene sus propios costos. Por lo general, 1) envías una respuesta HTML más grande que puede impulsar nuestra interactividad, 2) puedes dejar al usuario en un valle inquietante en el que la mitad de la experiencia no puede ser interactiva hasta que JavaScript termina de procesarse.

El inicio automático progresivo puede ser un mejor enfoque. Envía una página mínimamente funcional (compuesta solo por el código HTML/JS/CSS necesario para la ruta actual). A medida que llegan más recursos, la app puede cargar de forma diferida y desbloquear más funciones.

ALT_TEXT_HERE
Inicio progresivo, de Paul Lewis

Cargar código proporcional a lo que está a la vista es el Santo Grial. PRPL y el prearranque progresivo son patrones que pueden ayudar a lograrlo.

Conclusiones

El tamaño de transmisión es fundamental para las redes de gama baja. El tiempo de análisis es importante para los dispositivos vinculados a la CPU. Es importante mantener estos valores bajos.

Los equipos han tenido éxito con la adopción de presupuestos de rendimiento estrictos para mantener bajos los tiempos de transmisión y análisis/compilación de JavaScript. Consulta “Can You Afford It?” de Alex Russell: Presupuestos de rendimiento web en el mundo real" para obtener orientación sobre los presupuestos para dispositivos móviles.

ALT_TEXT_HERE
Es útil considerar cuánto “margen” de JS nos pueden dejar las decisiones de arquitectura que tomamos para la lógica de la app.

Si estás compilando un sitio que se orienta a dispositivos móviles, haz todo lo posible por desarrollarlo en hardware representativo, mantén bajos los tiempos de análisis y compilación de JavaScript y adopta un presupuesto de rendimiento para asegurarte de que tu equipo pueda supervisar sus costos de JavaScript.

Más información