JavaScript de división de código

La carga de recursos de JavaScript grandes afecta significativamente la velocidad de la página. Dividir tu código 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 tu página, lo que, a su vez, puede mejorar la Interacción con la siguiente pintura (INP) de tu página.

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 HTML inicial de una página y se les aplica un diseño con CSS. Sin embargo, es posible que el código JavaScript necesario para potenciar esos elementos interactivos, así como otros códigos que carga la página, estén analizando y ejecutando el código JavaScript para que funcionen. El resultado es que el usuario puede sentir que la interacción se retrasó significativamente o incluso que se interrumpió 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 tarda demasiado, es posible que los elementos interactivos de la página no respondan con la suficiente rapidez a la entrada del usuario. Una solución para esto es cargar solo el código JavaScript que necesitas para que la página funcione y diferir la carga de otro código JavaScript para más adelante con una técnica conocida como división de código. Este módulo se enfoca 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 arroja 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 problema potencial en cualquier punto del ciclo de vida de la página, ya que pueden aumentar la demora en la entrada de una interacción si el momento en 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 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á altamente correlacionado con el INP, lo que sugiere que los usuarios tienden a intentar interacciones 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 porque puede ayudarte a identificar exactamente qué secuencias de comandos pueden ser candidatas para la división de código. Luego, puedes ir más allá con la herramienta de cobertura en Herramientas para desarrolladores de Chrome para identificar exactamente qué partes del código JavaScript de una página no se usan durante la carga de la página.

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 en la carga de la página, por lo que no se puede cargar en ningún otro momento.
  • Es el JavaScript restante que se puede cargar en un momento posterior, por lo general, cuando el usuario interactúa con un elemento interactivo determinado en la página.

La división del código se puede realizar con la sintaxis dynamic import(). Esta sintaxis, a diferencia de los elementos <script> que solicitan un recurso de JavaScript determinado durante el inicio, realiza una solicitud para un recurso de JavaScript en un momento posterior del 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 desenfoca cualquiera de los campos <input> de un formulario. En esta situación, el recurso de 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 encuentran una llamada dinámica a import() en tu código fuente. La mayoría de estas herramientas lo hacen automáticamente, pero esbuild en particular requiere que habilites esta optimización.

Notas útiles sobre la división del 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, es conveniente tener en cuenta algunas cosas si decides auditar tu código fuente de JavaScript en busca de oportunidades de división de código.

Usa un agrupador si puedes

Es una práctica 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 subóptimas que pueden surgir cuando se envían módulos de JavaScript a producción.

Lo más importante es que debes usar un bundler para procesar y optimizar tu código fuente, incluidos los módulos que planeas dividir en código. Los bundlers son muy eficaces no solo para aplicar optimizaciones al código fuente de JavaScript, sino también para equilibrar las consideraciones de rendimiento, como el tamaño del paquete, en función de la relación de compresión. La eficacia de la compresión aumenta con el tamaño del paquete, pero los bundlers también intentan garantizar que los paquetes no sean tan grandes como para generar tareas largas debido a la evaluación de secuencias de comandos.

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

No inhabilitar la compilación de transmisión de forma involuntaria

El motor V8 de JavaScript de Chromium ofrece varias optimizaciones listas para usar para 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 transmitidos de JavaScript a medida que llegan de la red.

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

  • Transforma tu código de producción para evitar el uso de módulos de JavaScript. Los bundlers pueden transformar tu código fuente de JavaScript según un objetivo de compilación, y el objetivo suele ser específico para un entorno determinado. V8 aplicará la compilación de transmisión a cualquier código de JavaScript que no use módulos, y puedes configurar tu bundler para transformar tu código de módulo de JavaScript en una sintaxis que no use módulos de JavaScript ni 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 ningún tipo de contenido especial para el JavaScript que usa módulos en comparación con el JavaScript que no los usa. En lo que respecta a V8, dejas de usar la compilación de transmisión cuando envías módulos de JavaScript en producción con la extensión .js. Si usas la extensión .mjs para los módulos de JavaScript, V8 puede garantizar que no se interrumpa la compilación de transmisión para el código de JavaScript basado en módulos.

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

Demostración de importación dinámica

webpack

webpack se incluye con un complemento llamado SplitChunksPlugin, que te permite configurar cómo el bundler divide los archivos JavaScript. webpack reconoce las instrucciones dinámicas import() y estáticas import. El comportamiento de SplitChunksPlugin se puede modificar especificando la opción chunks en su configuración:

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

De forma predeterminada, cada vez que webpack encuentra una instrucción import() dinámica, crea un fragmento independiente 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 de webpack para el fragmento de código anterior genera dos fragmentos separados:

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

Esta configuración te permite aplazar la carga del fragmento form-validation.js hasta que realmente se necesite. Esto puede mejorar la capacidad de respuesta de la carga, ya que reduce el 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 producen cuando se cumple una condición especificada, en cuyo caso se descarga el módulo importado de forma dinámica. Un ejemplo podría ser una condición en la que un polyfill solo se descarga para un navegador en particular o, como en el ejemplo anterior, el módulo importado es necesario para una interacción del usuario.

Por otro lado, cambiar la configuración de SplitChunksPlugin para especificar chunks: initial garantiza que el código solo se divida en fragmentos iniciales. Estos son fragmentos como los que se importan de forma estática o se enumeran en la propiedad entry de webpack. Si observamos 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 podría generar un rendimiento de carga inicial de la página potencialmente peor.

Las opciones para SplitChunksPlugin también se pueden configurar para separar los scripts más grandes en varios más pequeños, por ejemplo, usando la opción maxSize para indicarle a webpack que divida los fragmentos en archivos separados si superan lo que especifica maxSize. Dividir los archivos de secuencias 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 es menos probable que bloqueen el subproceso principal durante períodos más largos.

Además, generar archivos JavaScript más grandes también significa que es más probable que las secuencias de comandos sufran invalidación de caché. Por ejemplo, si envías un script muy grande con código de framework y de la aplicación de origen, se puede invalidar todo el paquete si solo se actualiza el framework, pero no se actualiza nada más en el recurso empaquetado.

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 genera cargas de página más rápidas en las 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áginas 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 webpack.

Ponga a prueba sus conocimientos

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

Dinámico import().
Correcto.
Estático import.
Vuelve a intentarlo.

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

Dinámico import().
Vuelve a intentarlo.
Estático import.
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ámico, y los fragmentos initial se cargan con import estático.
Correcto.
Los fragmentos async se cargan con import estático, y los fragmentos initial se cargan con import() dinámico.
Vuelve a intentarlo.

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

Si bien tiende a ser un tipo de recurso bastante costoso, JavaScript no es el único tipo de recurso cuya carga puedes diferir. Los elementos de imagen y <iframe> son recursos potencialmente costosos por sí mismos. Al igual que en JavaScript, puedes diferir la carga de imágenes y el elemento <iframe> con la carga diferida, que se explica en el próximo módulo de este curso.