Evaluación de secuencias de comandos y tareas largas

Cuando se cargan secuencias de comandos, el navegador tarda en evaluarlas antes de la ejecución, 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 cuando se carga la página.

Cuando se trata de optimizar Interaction to Next Paint (INP), la mayoría de los consejos que encontrarás es optimizar las interacciones ellos mismos. Por ejemplo, en la guía de optimización de tareas largas, se analizan técnicas como el rendimiento con setTimeout y isInputPending, entre otras. Estas técnicas son beneficiosas, ya que le permiten al subproceso principal evitar las tareas largas, lo que puede brindar más oportunidades para que las interacciones y otras actividades se ejecuten antes, en lugar de tener que esperar una sola tarea larga.

Sin embargo, ¿qué ocurre con las tareas largas que surgen de la carga de secuencias de comandos? Estas tareas pueden interferir con las interacciones del usuario y afectar el INP de una página durante la carga. En esta guía se explorará cómo los navegadores manejan las tareas iniciadas por la evaluación de secuencias de comandos, y se analizará lo que puedes hacer para dividir el trabajo de evaluación de secuencias de comandos, de modo que tu subproceso principal pueda responder mejor a la entrada del usuario mientras se carga la página.

¿Qué es la evaluación de guiones?

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 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 para responder a las interacciones del usuario.
La evaluación de secuencias de comandos se muestra en el generador de perfiles de rendimiento de las Herramientas para desarrolladores de Chrome. En este caso, el trabajo es suficiente para provocar una tarea larga que impide que el subproceso principal realice otras tareas, 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, la secuencia de comandos se compila en bytecode y puede continuar con la ejecución.

Si bien es necesaria, la evaluación de secuencias de comandos puede ser problemática, ya que los usuarios pueden intentar interactuar con una página poco después de que se renderice. Sin embargo, el hecho de que una página se haya renderizado no significa que haya terminado de cargarse. Las interacciones que ocurren durante la carga pueden retrasarse porque la página está ocupada evaluando secuencias de comandos. Si bien no hay garantía de que se produzca la interacción deseada en este momento (ya que es posible que aún no se haya cargado una secuencia de comandos responsable de ella), podría haber interacciones que dependan de JavaScript y que estén listas, o bien que la interactividad no dependa en absoluto de JavaScript.

La relación entre los guiones y las tareas que los 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 mediante un elemento <script> normal o si una secuencia de comandos es un módulo cargado con type=module. Dado que los navegadores tienden a manejar las cosas de otra forma, se abordará la manera en que los principales motores de navegador manejan la evaluación de secuencias de comandos, donde varían los comportamientos de evaluación de secuencias de comandos.

Carga secuencias de comandos con el elemento <script>

En general, la cantidad de tareas enviadas para 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, es de esperar que se envíe una sola tarea para evaluar esa secuencia de comandos. ¿Esto es malo? No necesariamente, a menos que la secuencia de comandos sea enorme.

Puedes dividir el trabajo de evaluación de secuencias de comandos si evitas cargar grandes fragmentos de JavaScript. Además, puedes cargar secuencias de comandos individuales 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 el subproceso principal en absoluto, o al menos menos de lo que comenzaste.

Múltiples tareas relacionadas con 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 con mayor rapidez.
Se generaron varias tareas para evaluar secuencias de comandos como resultado de varios elementos <script> presentes en el código HTML de la página. Es preferible usar este método en lugar de enviar un gran paquete de secuencias de comandos a los usuarios, ya que es más probable que bloquee el subproceso principal.

Puedes pensar que dividir las tareas para la evaluación de secuencias de comandos es 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 producción divide el JavaScript que cargas en varias secuencias de comandos más pequeñas, en lugar de una cantidad menor de secuencias de comandos más grandes que lo que es más probable que bloquee el subproceso principal.

Carga de secuencias de comandos 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 de la experiencia para los desarrolladores, como no tener que transformar el código para su uso en producción, especialmente cuando se usa en combinación con importaciones de mapas. Sin embargo, esta carga de secuencias de comandos programa tareas que difieren de un navegador a otro.

Navegadores basados en Chromium

En navegadores como Chrome (o los derivados de él), cargar módulos ES con el atributo type=module produce tipos de tareas diferentes de los que se observan normalmente cuando no se usa type=module. Por ejemplo, se ejecutará una tarea para cada secuencia de comandos del módulo que implica una actividad etiquetada como Compile module.

La compilación de módulos funciona en varias tareas, como se muestra en las Herramientas para desarrolladores de Chrome.
Comportamiento de carga de módulos en navegadores basados en Chromium. Cada secuencia de comandos del módulo generará una llamada al módulo de compilación para compilar su contenido antes de la evaluación.

Una vez compilados los módulos, cualquier código que se ejecute posteriormente en ellos iniciará la 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 de un módulo, este se evalúa justo a tiempo.

El efecto, al menos en Chrome y los navegadores relacionados, es que los pasos de compilación se dividen cuando se usan módulos de ES. Esta es una clara victoria en términos de administración de tareas largas; sin embargo, el trabajo de evaluación del módulo resultante que resulta aún significa que estás incurriendo en un costo inevitable. Si bien debes esforzarte por ofrecer la menor cantidad de JavaScript posible, el uso de módulos ES, independientemente del navegador, ofrece los siguientes beneficios:

  • Todo el código del módulo se ejecuta automáticamente en el modo estricto, lo que permite posibles optimizaciones por parte de los motores de JavaScript que, de lo contrario, no se podrían realizar en un contexto no estricto.
  • Las secuencias de comandos que se cargan con type=module se tratan como si se diferiran de forma predeterminada. Es posible usar el atributo async en 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 de ellos se evalúa en una tarea independiente. Esto significa que, en teoría, podrías cargar un único módulo de nivel superior que contenga solo sentencias import estáticas a otros módulos, y cada módulo cargado incurrirá en una solicitud de red y una tarea de red independientes para evaluarlo.

Carga de secuencias de comandos con import() dinámico

Otro método para cargar secuencias de comandos es import() dinámico. A diferencia de las sentencias import estáticas, 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.

El import() dinámico ofrece dos ventajas para 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 al reducir 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 a import(), cada llamada separará efectivamente 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 secuencia 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 import() dinámicas se comportan de manera similar en todos los motores de navegador principales: el resultado de las tareas de evaluación de secuencias de comandos será la misma que la cantidad de módulos que se importan de forma dinámica.

Cómo cargar secuencias de comandos 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 este 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, el código que está dentro del trabajador web no lo hace. Esto reduce la congestión del subproceso principal y puede ayudar a mantenerlo más responsivo a las interacciones del usuario.

Además de reducir el trabajo en el subproceso principal, los trabajadores web por su cuenta pueden cargar secuencias de comandos externas para usarlas en su contexto de trabajadores, ya sea a través de instrucciones importScripts o import estáticas en navegadores que admiten trabajadores de módulos. El resultado es que cualquier secuencia de comandos solicitada por un trabajador web se evalúa fuera del subproceso principal.

Concesiones y consideraciones

Si bien dividir tus secuencias de comandos en archivos 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 a la hora de decidir cómo dividir las secuencias de comandos.

Eficiencia de la compresión

La compresión es un factor cuando se trata de 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, es una especie de equilibrio para garantizar que se dividan las secuencias de comandos en partes más pequeñas para facilitar una mejor interactividad durante el inicio.

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

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

Invalidación de caché

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

Al dividir tus secuencias de comandos, no solo divides el trabajo de evaluación de secuencias de comandos en tareas más pequeñas, sino que también estás aumentando 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 general de la página 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. La anidación de módulos hace referencia a los casos en los que 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 ES no están agrupados, el código anterior genera una cadena de solicitudes de red: cuando se solicita a.js desde un elemento <script>, se envía otra solicitud de red para b.js, que implica otra solicitud de c.js. Una forma de evitar esto es usar un agrupador, pero asegúrate de configurar tu agrupador para dividir las 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 evitar las llamadas a módulos anidados es usar la sugerencia de recursos modulepreload, que precargará los módulos de ES con anticipación para evitar cadenas de solicitudes de red.

Conclusión

No hay duda de que optimizar la evaluación de las secuencias de comandos en el navegador es una tarea complicada. El enfoque depende de los requisitos y las limitaciones de tu sitio web. Sin embargo, al dividir las secuencias de comandos, se distribuye el trabajo de la evaluación de la secuencia de comandos en numerosas tareas más pequeñas y, por lo tanto, se le brinda al subproceso principal la capacidad de manejar 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 las tareas de evaluación de secuencias de comandos grandes:

  • Cuando cargues secuencias de comandos con el elemento <script> sin el atributo type=module, evita cargar secuencias de comandos que sean muy grandes, ya que iniciarán tareas de evaluación de secuencias de comandos que requieren muchos recursos y bloquean el subproceso principal. Extiende tus secuencias de comandos en 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 de evaluación para cada secuencia de comandos del módulo por separado.
  • Reduce el tamaño de tus paquetes iniciales mediante llamadas import() dinámicas. Esto también funciona en los agrupadores, ya que los agrupadores tratarán cada módulo importado de forma dinámica como un “punto de división”, lo que generará una secuencia de comandos independiente para cada módulo importado de forma dinámica.
  • Asegúrate de sopesar las ventajas y desventajas, como la eficiencia de la compresión y la invalidación de la caché. Las secuencias de comandos más grandes se comprimen mejor, pero es más probable que impliquen trabajos de evaluación de secuencias de comandos más costosos en menos tareas y resulten en la invalidación de la caché del navegador, lo que disminuye la eficiencia general del almacenamiento en caché.
  • Si usas módulos ES de forma nativa sin agrupar, utiliza la sugerencia del recurso modulepreload para optimizar su carga durante el inicio.
  • Como siempre, envía el menor código JavaScript posible.

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

Hero image de Unsplash, de Markus Spiske.