Evaluación de secuencias de comandos y tareas largas

Cuando cargas secuencias de comandos, el navegador tarda un tiempo en evaluarlas antes de ejecutarlas, lo que puede generar tareas largas. Aprende cómo funciona la evaluación de secuencias de comandos y qué puedes hacer para evitar que provoque tareas largas durante la carga de la página.

Cuando se trata de optimizar Interaction to Next Paint (INP), la mayoría de las sugerencias que encontrarás es optimizar las interacciones por sí mismos. Por ejemplo, en la guía para optimizar tareas largas, se analizan técnicas como generar rendimiento con setTimeout y otras. Estas técnicas son beneficiosas, ya que permiten que el hilo principal tenga algo de espacio, ya que evitan tareas largas, lo que puede permitir que las interacciones y otras actividades se ejecuten más rápido, en lugar de tener que esperar una sola tarea larga.

Sin embargo, ¿qué pasa con las tareas largas que provienen de la carga de secuencias de comandos? Estas tareas pueden interferir en las interacciones del usuario y afectar el INP de una página durante la carga. Esta guía explorará cómo los navegadores manejan las tareas iniciadas por la evaluación de la secuencia de comandos y veremos lo que puedes hacer para dividir el trabajo de evaluación de secuencias de comandos, de modo que tu subproceso principal pueda ser más receptivo a la entrada del usuario mientras se carga la página.

¿Qué es la evaluación de secuencias de comandos?

Si creaste el perfil de una aplicación que envía mucho JavaScript, es posible que hayas visto tareas largas en las que el culpable está etiquetado como Evaluar secuencia de comandos.

La evaluación de secuencias de comandos funciona tal como se visualiza en el generador de perfiles de rendimiento de las Herramientas para desarrolladores de Chrome. El trabajo genera una tarea larga durante el inicio, lo que bloquea la capacidad del subproceso principal de responder a las interacciones del usuario.
La evaluación de secuencias de comandos funciona como se muestra en el generador de perfiles de rendimiento de las Herramientas para desarrolladores de Chrome. En este caso, el trabajo es suficiente para generar una tarea larga que impide que el subproceso principal realice otro trabajo, incluidas las tareas que impulsan las interacciones del usuario.

La evaluación de secuencias de comandos es una parte necesaria de la ejecución de JavaScript en el navegador, ya que JavaScript se compila justo a tiempo antes de la ejecución. Cuando se evalúa una secuencia de comandos, primero se analiza en busca de errores. Si el analizador no encuentra errores, se compilará la secuencia de comandos en un código de bytes y, luego, podrá continuar con la ejecución.

Si bien es necesario, la evaluación de las secuencias de comandos puede ser problemática, ya que es posible que los usuarios intenten interactuar con una página poco después de que se renderiza inicialmente. Sin embargo, el hecho de que una página se haya renderizado no significa que haya terminado de cargarse. Las interacciones que tienen lugar durante la carga pueden retrasarse porque la página está ocupada evaluando secuencias de comandos. Si bien no hay garantía de que se pueda producir una interacción en este momento (ya que es posible que aún no se haya cargado la secuencia de comandos responsable de ella), podría haber interacciones que dependen de JavaScript que estén listo o la interactividad no dependa en absoluto de JavaScript.

La relación entre las secuencias de comandos y las tareas que las evalúan

La forma en que se inician las tareas responsables de la evaluación de la secuencia de comandos depende de si la secuencia de comandos que cargas se carga con un elemento <script> típico o si es un módulo cargado con type=module. Dado que los navegadores tienden a manejar las cosas de forma diferente, la manera en que los principales motores de navegadores manejan la evaluación de las secuencias de comandos se tratará en qué partes varían los comportamientos de la evaluación de las secuencias de comandos entre ellos.

Secuencias de comandos cargadas con el elemento <script>

Por lo general, la cantidad de tareas enviadas a evaluar secuencias de comandos tiene una relación directa con la cantidad de elementos <script> de una página. Cada elemento <script> inicia una tarea para evaluar la secuencia de comandos solicitada de modo que se pueda analizar, compilar y ejecutar. Este es el caso de los navegadores basados en Chromium, Safari y Firefox.

¿Por qué este factor es importante? Supongamos que usas un agrupador para administrar tus secuencias de comandos de producción y lo configuraste para agrupar todo lo que tu página necesita para ejecutarse en una sola secuencia de comandos. Si este es el caso de tu sitio web, puedes esperar que se envíe una sola tarea para evaluar esa secuencia de comandos. ¿Es algo malo? No necesariamente, a menos que la secuencia de comandos sea enorme.

Puedes dividir el trabajo de evaluación de secuencias de comandos evitando cargar grandes fragmentos de JavaScript y cargar más secuencias de comandos individuales y más pequeñas con elementos <script> adicionales.

Si bien siempre debes esforzarte por cargar la menor cantidad de JavaScript posible durante la carga de la página, dividir tus secuencias de comandos garantiza que, en lugar de una tarea grande que pueda bloquear el subproceso principal, tengas una mayor cantidad de tareas más pequeñas que no bloquearán en absoluto el subproceso principal (o, al menos, menos que con lo que comenzaste).

Varias tareas que implican la evaluación de secuencias de comandos, tal como se visualiza en el generador de perfiles de rendimiento de las Herramientas para desarrolladores de Chrome. Debido a que se cargan varias secuencias de comandos más pequeñas en lugar de menos secuencias más grandes, es menos probable que las tareas se conviertan en tareas largas, lo que permite que el subproceso principal responda a la entrada del usuario más rápidamente.
Se generaron varias tareas para evaluar secuencias de comandos como resultado de múltiples elementos <script> presentes en el código HTML de la página. Es preferible enviar un paquete de secuencias de comandos grande a los usuarios, ya que tiene más probabilidades de bloquear el subproceso principal.

Puedes pensar que dividir las tareas para la evaluación de la secuencia de comandos es algo similar a generar durante devoluciones de llamada de eventos que se ejecutan durante una interacción. Sin embargo, con la evaluación de secuencias de comandos, el mecanismo de rendimiento divide el JavaScript que cargas en varias secuencias de comandos más pequeñas, en lugar de hacerlo en una cantidad menor de secuencias de comandos más grandes que las que probablemente bloqueen el subproceso principal.

Secuencias de comandos cargadas con el elemento <script> y el atributo type=module

Ahora es posible cargar módulos ES de forma nativa en el navegador con el atributo type=module en el elemento <script>. Este enfoque de carga de secuencias de comandos conlleva algunos beneficios para la experiencia del desarrollador, como no tener que transformar el código para su uso en producción, en especial cuando se usa en combinación con importos de mapas. Sin embargo, al cargar las secuencias de comandos de esta forma, se programan tareas que difieren de un navegador a otro.

Navegadores basados en Chromium

En navegadores como Chrome (o en los derivados de este), la carga de módulos de ES con el atributo type=module produce un tipo de tareas diferente de los que normalmente verías cuando no usas type=module. Por ejemplo, se ejecutará una tarea para la secuencia de comandos de cada módulo que involucre actividad etiquetada como Compile module.

La compilación de módulos funciona en varias tareas, tal como se visualiza en las Herramientas para desarrolladores de Chrome.
Comportamiento de carga de módulos en navegadores basados en Chromium. La secuencia de comandos de cada módulo generará una llamada a Compile module para compilar su contenido antes de la evaluación.

Una vez que se hayan compilado los módulos, el código que se ejecute posteriormente en ellos iniciará una actividad etiquetada como Evaluar módulo.

Evaluación justo a tiempo de un módulo como se muestra en el panel de rendimiento de las Herramientas para desarrolladores de Chrome.
Cuando se ejecuta el código en un módulo, este se evaluará en el momento.

El efecto aquí, al menos en Chrome y en los navegadores relacionados, es que los pasos de compilación se interrumpen al usar módulos ES. Esta es una clara victoria en términos de administración de tareas largas; sin embargo, el trabajo de evaluación de módulos resultante que resulta aún significa que se están incurriendo en costos inevitables. Si bien debes esforzarte por enviar la menor cantidad de JavaScript posible, el uso de módulos de ES, independientemente del navegador, brinda los siguientes beneficios:

  • Todo el código del módulo se ejecuta automáticamente en modo estricto, lo que permite posibles optimizaciones por parte de motores de JavaScript que, de otra manera, no podrían realizarse en un contexto no estricto.
  • Las secuencias de comandos que se cargan con type=module se tratan como si se diferieran de forma predeterminada. Es posible usar el atributo async en las secuencias de comandos cargadas con type=module para cambiar este comportamiento.

Safari y Firefox

Cuando los módulos se cargan en Safari y Firefox, cada uno se evalúa en una tarea separada. Esto significa que, en teoría, podrías cargar un solo módulo de nivel superior compuesto únicamente por instrucciones import estáticas a otros módulos, y cada módulo que se cargue generará una solicitud de red y una tarea independientes para evaluarlo.

Secuencias de comandos cargadas con import() dinámico

El import() dinámico es otro método para cargar secuencias de comandos. A diferencia de las sentencias estáticas import, que deben estar en la parte superior de un módulo ES, una llamada import() dinámica puede aparecer en cualquier lugar de una secuencia de comandos para cargar un fragmento de JavaScript a pedido. Esta técnica se denomina división de código.

Los import() dinámicos tienen dos ventajas cuando se trata de mejorar el INP:

  1. Los módulos que se aplazan para cargarse más tarde reducen la contención del subproceso principal durante el inicio, ya que reducen la cantidad de JavaScript que se carga en ese momento. Esto libera el subproceso principal para que pueda responder mejor a las interacciones del usuario.
  2. Cuando se realizan llamadas dinámicas de import(), cada llamada separará eficazmente la compilación y la evaluación de cada módulo en su propia tarea. Por supuesto, un import() dinámico que carga un módulo muy grande iniciará una tarea de evaluación de secuencias de comandos bastante grande, lo que puede interferir en la capacidad del subproceso principal de responder a la entrada del usuario si la interacción ocurre al mismo tiempo que la llamada dinámica a import(). Por lo tanto, sigue siendo muy importante que cargues la menor cantidad de JavaScript posible.

Las llamadas dinámicas de import() se comportan de manera similar en todos los motores de navegador principales: las tareas de evaluación de secuencias de comandos que se generan serán las mismas que la cantidad de módulos que se importan de forma dinámica.

Secuencias de comandos cargadas en un trabajador web

Los trabajadores web son un caso de uso especial de JavaScript. Los trabajadores web se registran en el subproceso principal, y el código dentro de estos se ejecuta en su propio subproceso. Esto es muy beneficioso en el sentido de que, si bien el código que registra el trabajador web se ejecuta en el subproceso principal, no lo hace el código del trabajador web. Esto reduce la congestión del subproceso principal y puede ayudar a que este último responda mejor a las interacciones del usuario.

Además de reducir el trabajo del subproceso principal, los trabajadores web por sí mismos pueden cargar secuencias de comandos externas para usar en el contexto del trabajador, ya sea mediante importScripts o sentencias import estáticas en navegadores que admiten trabajadores de módulos. El resultado es que cualquier secuencia de comandos que solicita un trabajador web se evalúa fuera del subproceso principal.

Concesiones y consideraciones

Si bien dividir tus secuencias de comandos en archivos separados y más pequeños ayuda a limitar las tareas largas en lugar de cargar menos archivos mucho más grandes, es importante tener en cuenta algunos aspectos al decidir cómo dividir las secuencias de comandos.

Eficiencia de la compresión

La compresión es un factor cuando se intenta dividir secuencias de comandos. Cuando las secuencias de comandos son más pequeñas, la compresión se vuelve algo menos eficiente. Las secuencias de comandos más grandes se beneficiarán mucho más de la compresión. Si bien aumentar la eficiencia de la compresión ayuda a mantener los tiempos de carga de las secuencias de comandos lo más bajos posible, garantizar que se dividan las secuencias de comandos en fragmentos más pequeños para facilitar una mejor interactividad durante el inicio es un acto de equilibrio.

Los agrupadores son herramientas ideales para administrar el tamaño de los resultados de las secuencias de comandos de las que depende tu sitio web:

  • En lo que respecta a webpack, su complemento SplitChunksPlugin puede ayudar. Consulta la documentación de SplitChunksPlugin para conocer las opciones que puedes configurar para administrar los tamaños de los recursos.
  • Para otros agrupadores, como Rollup y esbuild, puedes administrar los tamaños del archivo de secuencia de comandos mediante llamadas dinámicas import() en tu código. Estos agrupadores, al igual que webpack, separarán automáticamente el recurso importado de forma dinámica y lo colocarán en su propio archivo, lo que evitará tamaños iniciales de paquete más grandes.

Invalidación de caché

La invalidación de caché desempeña un papel importante en la velocidad de carga de una página en visitas repetidas. Cuando envías paquetes de secuencias de comandos grandes y monolíticos, tienes una desventaja con respecto al almacenamiento en caché del navegador. Esto se debe a que, cuando actualizas tu código propio, ya sea a través de la actualización de paquetes o de la corrección de errores en el envío, se invalida todo el paquete y se debe volver a descargar.

Al dividir tus secuencias de comandos, no solo estás dividiendo el trabajo de evaluación de secuencias de comandos en tareas más pequeñas, sino que también aumentas la probabilidad de que los visitantes recurrentes tomen más secuencias de comandos de la caché del navegador en lugar de la red. Esto se traduce en una carga de la página en general más rápida.

Módulos anidados y rendimiento de carga

Si envías módulos ES en producción y los cargas con el atributo type=module, debes tener en cuenta cómo el anidamiento de módulos puede afectar el tiempo de inicio. El anidamiento de módulos se refiere a cuando un módulo ES importa de forma estática otro módulo ES que importa de forma estática otro módulo ES:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

Si tus módulos de ES no están agrupados, el código anterior genera una cadena de solicitud de red: cuando se solicita a.js desde un elemento <script>, se despacha otra solicitud de red para b.js, que luego implica otra solicitud de c.js. Una forma de evitar esto es usar un agrupador, pero asegúrate de configurarlo para dividir secuencias de comandos a fin de distribuir el trabajo de evaluación de secuencias de comandos.

Si no quieres usar un agrupador, otra forma de eludir las llamadas de módulos anidados es usar la sugerencia del recurso modulepreload, que precargará los módulos ES con anticipación para evitar cadenas de solicitudes de red.

Conclusión

Optimizar la evaluación de secuencias de comandos en el navegador es, sin duda, una tarea complicada. El enfoque depende de los requisitos y las restricciones de tu sitio web. Sin embargo, al dividir las secuencias de comandos, se reparte el trabajo de evaluación de estas entre varias tareas más pequeñas y, por lo tanto, se otorga al subproceso principal la capacidad de controlar las interacciones del usuario de manera más eficiente, en lugar de bloquear el subproceso principal.

En resumen, aquí hay algunas cosas que puedes hacer para dividir tareas grandes de evaluación de secuencias de comandos:

  • Cuando cargues secuencias de comandos con el elemento <script> sin el atributo type=module, evita cargar secuencias de comandos muy grandes, ya que estas iniciarán tareas de evaluación de secuencias de comandos que consumen muchos recursos y bloquean el subproceso principal. Expande tus secuencias de comandos entre más elementos <script> para dividir este trabajo.
  • Si usas el atributo type=module para cargar módulos de ES de forma nativa en el navegador, se iniciarán tareas individuales para la evaluación de cada secuencia de comandos del módulo por separado.
  • Reduce el tamaño de los paquetes iniciales mediante llamadas dinámicas import(). Esto también funciona en agrupadores, ya que estos tratarán cada módulo importado de forma dinámica como un “punto de división”, lo que generará una secuencia de comandos separada para cada módulo importado de forma dinámica.
  • Asegúrate de considerar las ventajas y desventajas, como la eficiencia de la compresión y la invalidación de caché. Las secuencias de comandos más grandes se comprimen mejor, pero es más probable que involucren un trabajo de evaluación de secuencias de comandos más costoso en menos tareas y generen la invalidación de la caché del navegador, lo que lleva a una eficiencia general de almacenamiento en caché menor.
  • Si usas módulos ES de forma nativa sin agrupar, usa la sugerencia del recurso modulepreload para optimizar su carga durante el inicio.
  • Como siempre, envía la menor cantidad de JavaScript posible.

Sin duda, es un acto de equilibrio, pero si divides las secuencias de comandos y reduces las cargas útiles iniciales con import() dinámicos, puedes lograr un mejor rendimiento de inicio y adaptar mejor las interacciones de los usuarios durante ese período de inicio crucial. Esto debería ayudarte a obtener una mejor puntuación en la métrica INP y, así, ofrecer una mejor experiencia del usuario.

Hero image de Unsplash, de Markus Spiske.