JavaScript de división de código

La carga de grandes recursos de JavaScript tiene un impacto significativo en la velocidad de la página. Dividir tu JavaScript en fragmentos más pequeños y descargar solo lo necesario para que una página funcione durante el inicio puede mejorar en gran medida la capacidad de respuesta de carga de la página, lo que, a su vez, puede mejorar su Interacción al siguiente procesamiento de imagen (INP).

A medida que una página descarga, analiza y compila archivos JavaScript grandes, puede dejar de responder durante períodos. Los elementos de la página son visibles, ya que forman parte del código HTML inicial de una página y tienen el estilo CSS. Sin embargo, debido a que el código JavaScript necesario para potenciar esos elementos interactivos, así como otras secuencias de comandos que carga la página, puede analizar y ejecutar el código JavaScript para que funcionen. Como resultado, el usuario puede sentir que la interacción se retrasó significativamente o incluso se rompió por completo.

Esto suele suceder porque el subproceso principal está bloqueado, ya que JavaScript se analiza y compila en el subproceso principal. Si este proceso lleva demasiado tiempo, es posible que los elementos interactivos de la página no respondan con la rapidez suficiente a las entradas del usuario. Una solución para esto es cargar solo el código JavaScript que necesitas para que la página funcione y, al mismo tiempo, diferir otros elementos de JavaScript para cargarlos más adelante mediante una técnica conocida como división de código. Este módulo se centra en la última de estas dos técnicas.

Reduce el análisis y la ejecución de JavaScript durante el inicio a través de la división de código

Lighthouse lanza una advertencia cuando la ejecución de JavaScript tarda más de 2 segundos y falla cuando tarda más de 3.5 segundos. El análisis y la ejecución excesivos de JavaScript son un posible problema en cualquier punto del ciclo de vida de la página, ya que tiene el potencial de aumentar el retraso de entrada de una interacción si el tiempo en el que el usuario interactúa con la página coincide con el momento en que se ejecutan las tareas del subproceso principal responsables de procesar y ejecutar JavaScript.

Además, la ejecución y el análisis excesivos de JavaScript son particularmente problemáticos durante la carga inicial de la página, ya que este es el punto del ciclo de vida de la página en el que es muy probable que los usuarios interactúen con ella. De hecho, el Tiempo de bloqueo total (TBT), una métrica de capacidad de respuesta de carga, está muy correlacionada con INP, lo que sugiere que los usuarios tienden a intentar interactuar durante la carga inicial de la página.

La auditoría de Lighthouse que informa el tiempo dedicado a ejecutar cada archivo JavaScript que solicita tu página es útil, ya que puede ayudarte a identificar exactamente qué secuencias de comandos pueden ser candidatas para la división de código. Luego, puedes usar la herramienta de cobertura de las Herramientas para desarrolladores de Chrome a fin de identificar exactamente qué partes del JavaScript de una página no se usan durante la carga.

La división de código es una técnica útil que puede reducir las cargas útiles iniciales de JavaScript de una página. Te permite dividir un paquete de JavaScript en dos partes:

  • El código JavaScript necesario cuando se carga la página y, por lo tanto, no se puede cargar en ningún otro momento.
  • Es el JavaScript restante que se puede cargar más adelante, por lo general, cuando el usuario interactúa con un elemento interactivo determinado en la página.

La división de código se puede realizar con la sintaxis import() dinámica. Esta sintaxis, a diferencia de los elementos <script>, que solicitan un recurso de JavaScript determinado durante el inicio, realiza una solicitud de un recurso de JavaScript más adelante durante el ciclo de vida de la página.

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

En el fragmento de JavaScript anterior, el módulo validate-form.mjs se descarga, analiza y ejecuta solo cuando un usuario borra cualquiera de los campos <input> de un formulario. En esta situación, el recurso JavaScript responsable de controlar la lógica de validación del formulario solo se involucra con la página cuando es más probable que se use.

Los agrupadores de JavaScript, como webpack, Parcel, Rollup y esbuild, se pueden configurar para dividir los paquetes de JavaScript en fragmentos más pequeños cada vez que encuentren una llamada dinámica de import() en tu código fuente. La mayoría de estas herramientas hacen esto automáticamente, pero es necesario que habilites esta optimización para realizar compilaciones específicas.

Notas útiles sobre la división de código

Si bien la división de código es un método eficaz para reducir la contención del subproceso principal durante la carga inicial de la página, conviene tener en cuenta algunos aspectos si decides auditar el código fuente de JavaScript en busca de oportunidades de división de código.

Si puedes, usa un agrupador

Es común que los desarrolladores usen módulos de JavaScript durante el proceso de desarrollo. Es una excelente mejora de la experiencia del desarrollador que mejora la legibilidad y el mantenimiento del código. Sin embargo, existen algunas características de rendimiento deficientes que pueden generarse cuando se envían módulos de JavaScript a producción.

Lo más importante es usar un agrupador para procesar y optimizar el código fuente, incluidos los módulos que quieras dividir en el código. Los agrupadores son muy eficaces no solo para aplicar optimizaciones al código fuente de JavaScript, sino también bastante eficaces en el equilibrio de consideraciones de rendimiento, como el tamaño del paquete y la proporción de compresión. La eficacia de la compresión aumenta con el tamaño del paquete, pero los agrupadores también intentan asegurarse de que los paquetes no sean tan grandes como para incurrir en tareas largas debido a la evaluación de secuencias de comandos.

Los agrupadores también evitan el problema de enviar una gran cantidad de módulos sin empaquetar a través de la red. Las arquitecturas que usan módulos de JavaScript suelen tener árboles de módulos grandes y complejos. Cuando se desagrupan los árboles de módulos, cada módulo representa una solicitud HTTP independiente, y la interactividad de tu app web puede retrasarse si no agrupas módulos. Si bien es posible usar la sugerencia de recursos <link rel="modulepreload"> para cargar árboles de módulos grandes lo antes posible, se recomienda usar los paquetes de JavaScript desde el punto de vista del rendimiento de carga.

No inhabilites la compilación de transmisiones por error

El motor V8 de JavaScript de Chromium ofrece una serie de optimizaciones listas para usar a fin de garantizar que tu código JavaScript de producción se cargue de la manera más eficiente posible. Una de estas optimizaciones se conoce como compilación de transmisión que, al igual que el análisis incremental de HTML transmitido al navegador, compila fragmentos de JavaScript transmitidos a medida que llegan desde la red.

Existen dos formas de asegurarte de que la compilación de transmisión se realice para tu aplicación web en Chromium:

  • Transforma tu código de producción para evitar el uso de módulos de JavaScript. Los agrupadores pueden transformar el código fuente de JavaScript en función de un objetivo de compilación que suele ser específico de un entorno determinado. V8 aplicará la compilación de transmisiones a cualquier código JavaScript que no use módulos, y puedes configurar tu agrupador para transformar el código de tu módulo JavaScript en una sintaxis que no utilice módulos de JavaScript y sus funciones.
  • Si deseas enviar módulos de JavaScript a producción, usa la extensión .mjs. Independientemente de si tu JavaScript de producción usa módulos o no, no hay un tipo de contenido especial para JavaScript que use módulos en comparación con JavaScript que no los use. En lo que respecta a V8, puedes inhabilitar de manera efectiva la compilación de transmisiones cuando envías módulos de JavaScript en producción con la extensión .js. Si usas la extensión .mjs para módulos de JavaScript, V8 puede garantizar que no se rompa la compilación de transmisión del código JavaScript basado en módulos.

No permitas que estas consideraciones te disuadan de usar la división de código. La división de código es una manera eficaz de reducir las cargas útiles iniciales de JavaScript para los usuarios. No obstante, si usas un agrupador y sabes cómo conservar el comportamiento de compilación de transmisiones de V8, puedes asegurarte de que tu código de JavaScript de producción sea lo más rápido posible para los usuarios.

Demostración de importación dinámica

webpack

webpack se envía con un complemento llamado SplitChunksPlugin, que te permite configurar la forma en que el agrupador divide los archivos JavaScript. Webpack reconoce las sentencias import() dinámicas y import estáticas. Se puede modificar el comportamiento de SplitChunksPlugin si especificas la opción chunks en su configuración:

  • chunks: async es el valor predeterminado y hace referencia a las llamadas dinámicas import().
  • chunks: initial hace referencia a las llamadas estáticas import.
  • chunks: all abarca las importaciones dinámicas y estáticas de import(), lo que te permite compartir fragmentos entre las importaciones async y initial.

De forma predeterminada, cada vez que webpack encuentra una declaración import() dinámica, crea un fragmento separado para ese módulo:

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

La configuración predeterminada del paquete web para el fragmento de código anterior da como resultado dos fragmentos separados:

  • El fragmento main.js, que webpack clasifica como un fragmento initial, que incluye los módulos main.js y ./my-function.js
  • El fragmento async, que incluye solo form-validation.js (que contiene un hash de archivo en el nombre del recurso, si está configurado). Este fragmento solo se descarga si condition es verdadero.

Esta configuración te permite diferir la carga del fragmento form-validation.js hasta que sea realmente necesario. Esto puede mejorar la capacidad de respuesta de la carga mediante la reducción del tiempo de evaluación de la secuencia de comandos durante la carga inicial de la página. La descarga y evaluación de la secuencia de comandos para el fragmento form-validation.js se produce cuando se cumple una condición especificada, en cuyo caso, se descarga el módulo importado de forma dinámica. Un ejemplo puede ser una condición en la que solo se descarga un polyfill para un navegador en particular o, como en el ejemplo anterior, el módulo importado es necesario para la interacción del usuario.

Por otro lado, cambiar la configuración de SplitChunksPlugin para especificar chunks: initial garantiza que el código se divida solo en los fragmentos iniciales. Estos son fragmentos como los que se importaron de forma estática o se enumeran en la propiedad entry de webpack. En el ejemplo anterior, el fragmento resultante sería una combinación de form-validation.js y main.js en un solo archivo de secuencia de comandos, lo que generaría un rendimiento potencialmente peor de la carga inicial de la página.

Las opciones de SplitChunksPlugin también se pueden configurar para separar las secuencias de comandos más grandes en varias más pequeñas, por ejemplo, mediante la opción maxSize a fin de indicarle a webpack que divida los fragmentos en archivos separados si superan lo que especifica maxSize. Dividir los archivos de secuencia de comandos grandes en archivos más pequeños puede mejorar la capacidad de respuesta de la carga, ya que en algunos casos el trabajo de evaluación de secuencias de comandos que requiere mucha CPU se divide en tareas más pequeñas, que tienen menos probabilidades de bloquear el subproceso principal durante períodos más largos.

Además, generar archivos JavaScript más grandes también implica que las secuencias de comandos tienen más probabilidades de sufrir una invalidación de caché. Por ejemplo, si envías una secuencia de comandos muy grande con código de framework y código de aplicación propio, se puede invalidar todo el paquete si solo se actualiza el framework, pero nada más en el recurso del paquete.

Por otro lado, los archivos de secuencia de comandos más pequeños aumentan la probabilidad de que un visitante recurrente recupere recursos de la caché, lo que acelera la carga de la página en visitas repetidas. Sin embargo, los archivos más pequeños se benefician menos de la compresión que los más grandes y pueden aumentar el tiempo de ida y vuelta de la red en las cargas de página con una caché del navegador no preparada. Se debe tener cuidado para lograr un equilibrio entre la eficiencia del almacenamiento en caché, la eficacia de la compresión y el tiempo de evaluación de la secuencia de comandos.

demostración de webpack

Demostración de SplitChunksPlugin de Webpack.

Pon a prueba tus conocimientos

¿Qué tipo de declaración import se usa cuando se realiza la división de código?

import() dinámico.
Correcto.
import estático.
Vuelve a intentarlo.

¿Qué tipo de declaración import debe estar en la parte superior de un módulo de JavaScript y en ninguna otra ubicación?

import() dinámico.
Vuelve a intentarlo.
import estático.
Correcto.

Cuando se usa SplitChunksPlugin en webpack, ¿cuál es la diferencia entre un fragmento async y un fragmento initial?

Los fragmentos async se cargan con import() dinámicos, y los fragmentos initial se cargan con import estáticos.
Correcto.
Los fragmentos async se cargan con import estáticos y los fragmentos initial se cargan con import() dinámicos.
Vuelve a intentarlo.

A continuación: Imágenes de carga diferida y elementos <iframe>

Si bien suele ser un tipo de recurso bastante costoso, JavaScript no es el único tipo de recurso que se puede diferir la carga. Los elementos Image y <iframe> son recursos potencialmente costosos por sí solos. Al igual que con JavaScript, puedes diferir la carga de imágenes y el elemento <iframe> cargando de forma diferida, lo que se explica en el siguiente módulo de este curso.