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 causar 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 la Interaction to Next Paint (INP), la mayoría de los consejos que encontrarás son para optimizar las interacciones en sí. Por ejemplo, en la guía para optimizar tareas largas, se analizan técnicas como la entrega 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, que pueden 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. En esta guía, se explorará cómo los navegadores controlan las tareas que inicia 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 para que tu subproceso principal pueda ser más responsivo a las entradas 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 causa 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 la secuencia 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 causar una tarea larga que bloquea el subproceso principal de realizar otro trabajo, incluidas las tareas que generan interacciones del usuario.

La evaluación de secuencias de comandos es una parte necesaria para ejecutar JavaScript en el navegador, ya que 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 una secuencia 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 se haya terminado de cargar. Las interacciones que se producen 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 realizar una interacción en este momento (ya que es posible que aún no se haya cargado la secuencia de comandos responsable), es posible que haya interacciones que dependan de JavaScript que estén listas, o bien que 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 controlar los elementos de manera diferente, se abordará cómo los principales motores de navegadores controlan la evaluación de secuencias de comandos, en la que los comportamientos de evaluación de secuencias de comandos varían entre ellos.

Secuencias de comandos cargadas con el elemento <script>

La cantidad de tareas enviadas para evaluar secuencias de comandos suele tener una relación directa con la cantidad de elementos <script> en 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 empaquetador para administrar tus secuencias de comandos de producción y lo configuraste para empaquetar 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 cargando secuencias de comandos más individuales y más pequeñas con elementos <script> adicionales.

Si bien siempre debes esforzarte por cargar la menor cantidad posible de JavaScript 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 que con lo que empezaste.

Varias tareas que involucran la evaluación de secuencias de comandos, como se visualiza en el generador de perfiles de rendimiento de Chrome DevTools. 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. Esto es preferible a enviar un paquete de secuencias de comandos grande a los usuarios, lo que es más probable que bloquee 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 cesión divide el código JavaScript que cargas en varias secuencias de comandos más pequeñas, en lugar de una menor cantidad de secuencias de comandos más grandes que tienen más probabilidades de bloquear 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 para la carga de secuencias de comandos tiene algunos beneficios para la experiencia del desarrollador, como no tener que transformar el código para el uso en producción, en especial cuando se usa en combinación con mapas de importación. 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 aquellos derivados de él, cargar módulos de ES con el atributo type=module produce diferentes tipos de tareas de las que normalmente ves cuando no usas type=module. Por ejemplo, se ejecutará una tarea para cada secuencia de comandos de módulo que involucre la actividad etiquetada como Compilar módulo.

El trabajo de compilación de módulos se realiza en varias tareas, como se visualiza 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 Compile module para compilar su contenido antes de la evaluación.

Una vez que se hayan compilado 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 visualiza 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 aquí, al menos en Chrome y navegadores relacionados, es que los pasos de compilación se dividen cuando se usan 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 cargadas con type=module se tratan como si estuvieran aplazadas 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 se cargan módulos en Safari y Firefox, cada uno se evalúa en una tarea independiente. Esto significa que, en teoría, podrías cargar un solo módulo de nivel superior que conste solo de sentencias import estáticas a otros módulos, y cada módulo cargado incurrirá en una solicitud y una tarea de red 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 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 on demand. 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 ser más responsivo a las interacciones del usuario.
  2. Cuando se realizan llamadas import() dinámicas, cada llamada separará de manera efectiva la compilación y la evaluación de cada módulo en su propia tarea. Por supuesto, un import() dinámico que cargue un módulo muy grande iniciará una tarea de evaluación de secuencia de comandos bastante grande, y eso puede interferir con la capacidad del subproceso principal para responder a la entrada del usuario si la interacción se produce al mismo tiempo que la llamada import() dinámica. Por lo tanto, sigue siendo muy importante que cargues la menor cantidad posible de JavaScript.

Las llamadas import() dinámicas 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 Web Workers se registran en el subproceso principal y, luego, el código dentro del trabajador se ejecuta en su propio subproceso. Esto es muy beneficioso en el sentido de que, mientras que el código que registra el trabajador web se ejecuta en el subproceso principal, el código dentro del trabajador web no lo hace. Esto reduce la congestión del subproceso principal y puede ayudar a que este sea más responsivo a las interacciones del usuario.

Además de reducir el trabajo del subproceso principal, los Web Workers pueden cargar secuencias de comandos externas para usarlas en el contexto del trabajador, ya sea a través de importScripts o de 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.

Ventajas 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 cuando decidas cómo dividir las secuencias de comandos.

Eficiencia de 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 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 un poco difícil lograr el equilibrio para asegurarte de dividir las secuencias de comandos en partes lo suficientemente pequeñas como para facilitar una mejor interactividad durante el inicio.

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 el caso de webpack, el complemento SplitChunksPlugin puede ser útil. Consulta la documentación de SplitChunksPlugin para conocer las opciones que puedes configurar para administrar los tamaños de los recursos.
  • En el caso de otros empaquetadores, como Rollup y esbuild, puedes administrar los tamaños de los archivos de secuencia de comandos mediante llamadas import() dinámicas en tu código. Estos empaquetadores, al igual que Webpack, dividirán automáticamente el recurso importado de forma dinámica en su propio archivo, lo que evitará que los tamaños iniciales del paquete sean más grandes.

Invalidación de caché

La invalidación de la caché desempeña un papel importante en la velocidad con la que se carga una página en las visitas repetidas. Cuando envías paquetes de secuencias de comandos monolíticas grandes, tienes una desventaja en cuanto a la 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 el envío de correcciones de errores), se invalida todo el paquete y se debe volver a descargar.

Cuando divides 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 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 página más rápida en general.

Módulos anidados y rendimiento de carga

Si envías módulos de 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 de ES importa de forma estática otro módulo de ES que importa de forma estática otro módulo de 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 solicitudes de red: cuando se solicita a.js desde un elemento <script>, se envía otra solicitud de red para b.js, que luego implica otra solicitud para c.js. Una forma de evitar esto es usar un empaquetador, pero asegúrate de configurarlo para que divida las secuencias de comandos y distribuya el trabajo de evaluación de secuencias de comandos.

Si no quieres usar un agrupador, otra forma de eludir las llamadas a 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, cuando divides las secuencias de comandos, distribuyes el trabajo de evaluación de secuencias de comandos en varias tareas más pequeñas y, por lo tanto, le das al subproceso principal la capacidad de controlar las interacciones del usuario de manera más eficiente, en lugar de bloquearlo.

A modo de resumen, estas son algunas de las acciones que puedes realizar para dividir 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 muy grandes, ya que iniciarán tareas de evaluación de secuencias de comandos que requieren muchos recursos y que bloquean el subproceso principal. Distribuye tus secuencias de comandos en más elementos <script> para dividir este trabajo.
  • El uso del atributo type=module para cargar módulos de ES de forma nativa en el navegador iniciará tareas individuales para la evaluación de cada secuencia de comandos de módulo independiente.
  • Reduce el tamaño de tus paquetes iniciales con llamadas import() dinámicas. 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 sopesar las compensaciones, 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 un trabajo de evaluación de secuencias de comandos más costoso en menos tareas y que generen invalidación de la caché del navegador, lo que reduce la eficiencia general de la caché.
  • Si usas módulos ES de forma nativa sin agruparlos, usa la sugerencia de recursos modulepreload para optimizar su carga durante el inicio.
  • Como siempre, envía la menor cantidad posible de JavaScript.

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