Cuando se cargan secuencias de comandos, el navegador tarda en evaluarlas antes de la ejecución, lo que puede causar tareas largas. Obtén información sobre cómo funciona la evaluación de secuencias de comandos y qué puedes hacer para evitar que cause tareas largas durante la carga de la página.
Cuando se trata de optimizar la Interaction to Next Paint (INP), la mayoría de las recomendaciones que encontrarás se centran en optimizar las interacciones en sí. Por ejemplo, en la guía para optimizar tareas largas, se analizan técnicas como la generación con setTimeout y otras. Estas técnicas son beneficiosas, ya que permiten que el subproceso principal tenga un poco de espacio al evitar tareas largas, lo que puede permitir 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é sucede 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 se inician con la evaluación de secuencias de comandos y se analizará qué 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 la secuencia de comandos?
Si generaste un perfil de una aplicación que incluye mucho código JavaScript, es posible que hayas visto tareas largas en las que el culpable se etiqueta como Evaluate Script.
La evaluación de la secuencia 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 para detectar errores. Si el analizador no encuentra errores, la secuencia de comandos se compila en código de bytes y, luego, 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 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 demorarse 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 una secuencia de comandos responsable de ella, podría haber interacciones que dependan de JavaScript y que estén listas, o bien la interactividad no dependa de JavaScript en absoluto.
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 con un elemento <script> típico o si la secuencia de comandos es un módulo que se carga con type=module. Dado que los navegadores tienden a manejar las cosas de manera diferente, se abordará cómo los principales motores de navegadores manejan la evaluación de secuencias de comandos cuando 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 los 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 el script solicitado, 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 bundler para administrar tus secuencias de comandos de producción y lo configuraste para que agrupe 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 ese script. ¿Es algo malo? No necesariamente, a menos que ese script sea enorme.
Puedes dividir el trabajo de evaluación de la secuencia de comandos evitando cargar grandes fragmentos de JavaScript y cargar más secuencias de comandos individuales más pequeñas con elementos <script> adicionales.
Si bien siempre debes intentar 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 puede 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 tenías al principio.
<script> 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 en la división de tareas para la evaluación de secuencias de comandos como algo similar a generar durante las 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 código 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 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 ofrece 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 mapas de importación. Sin embargo, cargar secuencias de comandos de esta manera programa tareas que difieren de un navegador a otro.
Navegadores basados en Chromium
En navegadores como Chrome (o los derivados de él), la carga de módulos ES con el atributo type=module genera diferentes tipos de tareas de las que normalmente verías cuando no usas type=module. Por ejemplo, se ejecutará una tarea para cada secuencia de comandos del módulo que involucre actividad etiquetada como Compile module.
Una vez que se compilan los módulos, cualquier código que se ejecute posteriormente en ellos iniciará la actividad etiquetada como Evaluate module.
El efecto aquí, al menos en Chrome y los navegadores relacionados, es que los pasos de compilación se interrumpen cuando se usan módulos ES. Esto es una clara ventaja en términos de administración de tareas largas. Sin embargo, el trabajo de evaluación del módulo resultante aún implica que incurres en un costo inevitable. Si bien debes intentar enviar la menor cantidad posible de JavaScript, usar módulos de ES, independientemente del navegador, proporciona los siguientes beneficios:
- Todo el código del módulo se ejecuta automáticamente en el modo estricto, lo que permite optimizaciones potenciales por parte de los motores de JavaScript que, de otro modo, no se podrían realizar en un contexto no estricto.
- De forma predeterminada, los scripts cargados con
type=modulese tratan como si se hubieran diferido. Es posible usar el atributoasyncen las secuencias de comandos cargadas contype=modulepara cambiar este comportamiento.
Safari y Firefox
Cuando se cargan los módulos en Safari y Firefox, cada uno de ellos se evalúa en una tarea independiente. Esto significa que, teóricamente, podrías cargar un solo módulo de nivel superior que conste solo de instrucciones import estáticas en otros módulos, y cada módulo cargado generará una solicitud de red y una tarea independientes para evaluarlo.
Secuencias de comandos cargadas con import() dinámico
Dynamic import() 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 de ES, una llamada import() dinámica puede aparecer en cualquier parte de una secuencia de comandos para cargar un fragmento de JavaScript a pedido. Esta técnica se denomina división del código.
El import() dinámico tiene dos ventajas cuando se trata de mejorar el INP:
- Los módulos cuya carga se aplaza para más adelante reducen la contención del subproceso principal durante el inicio, ya que disminuyen 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.
- Cuando se realizan llamadas dinámicas a
import(), cada llamada separará de manera efectiva la compilación y la evaluación de cada módulo en su propia tarea. Por supuesto, unimport()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 para responder a la entrada del usuario si la interacción ocurre al mismo tiempo que la llamada deimport()dinámico. Por lo tanto, sigue siendo muy importante que cargues la menor cantidad posible de JavaScript.
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 resultantes serán las mismas que la cantidad de módulos que se importan de forma dinámica.
Scripts cargados 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 el código dentro del worker se ejecuta en su propio subproceso. Esto es muy beneficioso, ya que, si bien 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 sensible a las interacciones del usuario.
Además de reducir el trabajo del subproceso principal, los web workers en sí pueden cargar secuencias de comandos externas para usarse en el contexto del worker, ya sea a través de importScripts o de instrucciones import estáticas en los navegadores que admiten workers de módulo. El resultado es que cualquier secuencia de comandos solicitada por un trabajador web se evalúa fuera del subproceso principal.
Compensaciones y consideraciones
Si bien dividir tus secuencias de comandos en archivos separados más pequeños ayuda a limitar las tareas largas en comparación con la carga de menos archivos mucho más grandes, es importante tener en cuenta algunas cosas cuando decidas cómo dividir las secuencias de comandos.
Eficiencia de compresión
La compresión es un factor a tener en cuenta cuando se trata de dividir los guiones. Cuando los scripts son más pequeños, la compresión se vuelve algo menos eficiente. Los scripts 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 necesario lograr un equilibrio para garantizar que las secuencias de comandos se dividan en suficientes fragmentos más pequeños para facilitar una mejor interactividad durante el inicio.
Los bundlers 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, su complemento
SplitChunksPluginpuede ser útil. Consulta la documentación deSplitChunksPluginpara conocer las opciones que puedes configurar para administrar los tamaños de los recursos. - Para otros bundlers, como Rollup y esbuild, puedes administrar los tamaños de los archivos de secuencia de comandos con llamadas dinámicas a
import()en tu código. Estos bundlers, así como webpack, separarán automáticamente el recurso importado de forma dinámica en su propio archivo, lo que evitará 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 rapidez con la que se carga una página en visitas repetidas. Cuando envías paquetes de secuencias de comandos grandes y monolíticos, tienes una desventaja en lo que respecta 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 el envío de correcciones de errores, todo el paquete se invalida y debe descargarse de nuevo.
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 aumentas la probabilidad de que los visitantes que regresan 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 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 de ES importa de forma estática otro módulo de ES que, a su vez, 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 bundler, 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 bundler, otra forma de evitar las llamadas a módulos anidados es usar la sugerencia de recurso modulepreload, que precargará los módulos de ES con anticipación para evitar cadenas de solicitudes de red.
Conclusión
Sin duda, optimizar la evaluación de secuencias de comandos en el navegador es una tarea difícil. El enfoque depende de los requisitos y las restricciones de tu sitio web. Sin embargo, al dividir los scripts, distribuyes el trabajo de evaluación de scripts en numerosas 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 bloquear el subproceso principal.
Para recapitular, estas son 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 atributotype=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. Distribuye tus secuencias de comandos en más elementos<script>para dividir este trabajo. - Usar el atributo
type=modulepara cargar módulos 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 usando llamadas
import()dinámicas. Esto también funciona en los bundlers, 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 independiente para cada módulo importado de forma dinámica. - Asegúrate de evaluar las ventajas y desventajas, como la eficiencia de la compresión y la invalidación de la caché. Los scripts más grandes se comprimirán mejor, pero es más probable que impliquen un trabajo de evaluación de scripts más costoso en menos tareas y que provoquen la invalidación de la caché del navegador, lo que generará una eficiencia de almacenamiento en caché general más baja.
- Si usas módulos de ES de forma nativa sin agruparlos, usa la sugerencia de recurso
modulepreloadpara optimizar su carga durante el inicio. - Como siempre, envía la menor cantidad posible de JavaScript.
Sin duda, es un acto de 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 adaptarte mejor a las interacciones del usuario durante ese período de inicio crucial. Esto debería ayudarte a obtener una mejor puntuación en la métrica del INP y, de este modo, brindar una mejor experiencia del usuario.