W3docs

JavaScript Dynamic import()

Aprende JavaScript dynamic import() — carga módulos bajo demanda para code splitting y lazy loading, con sintaxis await y .then(), manejo de errores y ejemplos ejecutables.

Las importaciones dinámicas en JavaScript son una característica introducida en ECMAScript 2020 (ES2020) que permite cargar un módulo en tiempo de ejecución, bajo demanda, en lugar de hacerlo todo de una vez cuando el script se analiza por primera vez. A diferencia de la sentencia import estática, la forma import() es una expresión similar a una función que devuelve una promesa, por lo que puedes decidir cuándo y si cargar un módulo en función de condiciones, acciones del usuario o enrutamiento. Esta guía cubre la sintaxis, los casos de uso más comunes (code splitting, lazy loading, carga condicional), el manejo de errores y un ejemplo completo ejecutable.

Este capítulo asume que te sientes cómodo con los módulos ES y async/await.

import estático vs. import() dinámico

Una sentencia import estática debe aparecer en el nivel superior de un módulo y se resuelve completamente antes de que se ejecute cualquier código del módulo. Eso la hace predecible y compatible con las herramientas, pero también significa que cada módulo importado estáticamente se obtiene desde el principio, incluso el código al que el usuario quizás nunca llegue.

import() es diferente en tres aspectos importantes:

  • Es una expresión, no una sentencia, por lo que puede aparecer en cualquier lugar: dentro de un if, una función o un manejador de eventos.
  • Acepta un especificador dinámico: la ruta del módulo puede ser una variable o una cadena calculada, no solo un literal de cadena.
  • Devuelve una promesa que se resuelve con el objeto de espacio de nombres del módulo (un object cuyas propiedades son las exportaciones nombradas del módulo, más default).
// Static import — runs at parse time, must be top-level
import { formatDate } from './utils.js';

// Dynamic import — runs when this line executes, can be anywhere
const utils = await import('./utils.js');
utils.formatDate(new Date());

Dado que import() devuelve una promesa, el resultado se maneja con await (dentro de una función async) o con .then()/.catch():

// With await
const mod = await import('./utils.js');

// With .then() / .catch()
import('./utils.js')
  .then(mod => mod.formatDate(new Date()))
  .catch(err => console.error('Failed to load module:', err));

Leer las exportaciones del objeto de módulo

El valor resuelto es el object de espacio de nombres del módulo. Las exportaciones nombradas son propiedades; la exportación predeterminada se encuentra bajo la clave default. La desestructuración hace esto más limpio:

// math.js exports: export function add(a,b){...}, export default function greet(){...}
const { add, default: greet } = await import('./math.js');

console.log(add(2, 3)); // 5
console.log(greet());   // "hello"
Información

await import(...) solo funciona dentro de una función async o en el nivel superior de un módulo ES (top-level await). En un <script> ordinario o en una función no asíncrona, utiliza la forma .then() en su lugar.

Casos de uso comunes

Las importaciones dinámicas destacan cuando una parte de tu aplicación se usa condicionalmente o no se necesita de inmediato. A continuación se muestran los patrones más comunes.

Code splitting

El caso de uso más común de las importaciones dinámicas es el code splitting: dividir tu bundle en fragmentos más pequeños que se cargan solo cuando se necesitan, típicamente cuando se visita una ruta o se usa una característica. A continuación, un script pesado se obtiene solo después de que el usuario hace clic, en lugar de inflar la carga inicial de la página.

button.addEventListener('click', function () {
    import('./heavyScript.js').then(mod => {
        mod.runHeavyTask();
    });
});

Dado que el manejador de clic es síncrono, aquí se usa la forma .then() en lugar de await. El navegador (o el bundler) solicita heavyScript.js solo en el primer clic; los clics posteriores reutilizan el módulo almacenado en caché.

Advertencia

Mide antes de dividir. Agregar demasiados fragmentos dinámicos pequeños puede perjudicar el rendimiento: cada uno es un viaje de red independiente. Reserva las importaciones dinámicas para código que sea genuinamente grande o rara vez utilizado.

Lazy loading de componentes

Frameworks como React, Angular y Vue usan importaciones dinámicas internamente para hacer lazy loading de componentes: un componente solo se obtiene cuando se renderiza por primera vez.

// Lazy loading a component in React
const LazyComponent = React.lazy(() => import('./LazyComponent'));

function App() {
    return (
        <React.Suspense fallback={<div>Loading...</div>}>
            <LazyComponent />
        </React.Suspense>
    );
}

React.lazy envuelve la importación dinámica, y React.Suspense muestra el fallback hasta que llega el fragmento. El usuario ve Loading... solo durante el breve momento en que se está obteniendo el componente.

Uso avanzado de las importaciones dinámicas

Carga condicional

Dado que import() es una expresión, puedes protegerla con cualquier condición: un indicador de característica, una configuración de usuario, el entorno o incluso la configuración regional del navegador.

if (user.prefersAdvancedMode) {
    const advanced = await import('./advancedEditor.js');
    advanced.init();
}

Los usuarios que nunca activan el modo avanzado nunca descargan advancedEditor.js. Puedes ir más lejos con un especificador dinámico: cargar un módulo diferente según la configuración regional, por ejemplo:

const locale = navigator.language.startsWith('fr') ? 'fr' : 'en';
const messages = await import(`./locales/${locale}.js`);
console.log(messages.default.greeting);
Advertencia

Los bundlers como Webpack y Vite necesitan saber qué archivos podrían cargarse. Un especificador completamente arbitrario (por ejemplo, una ruta construida a partir de la entrada del usuario) no puede ser procesado por el bundler. Mantén la parte variable de la ruta en un directorio y extensión conocidos, como en el ejemplo de configuración regional anterior.

Soporte en herramientas de build y Node.js

Cuando escribes import('./module.js'), los bundlers como Webpack, Rollup y Vite emiten automáticamente un fragmento separado y lo cargan bajo demanda; generalmente no se necesita configuración adicional. En el navegador, import() nativo es compatible con todos los navegadores modernos.

import() también funciona en Node.js (v12+), incluso dentro de archivos CommonJS, lo cual es la forma estándar de cargar un módulo ES desde código CommonJS:

// Loading an ESM module from a CommonJS file
async function run() {
    const { default: chalk } = await import('chalk');
    console.log(chalk.green('Loaded an ESM package from CommonJS'));
}
run();

Metadatos del módulo con import.meta

Dentro de un módulo puedes leer import.meta para obtener información contextual. El campo más ampliamente compatible es import.meta.url, que contiene la URL del módulo actual, útil para resolver recursos del mismo directorio:

// Resolve a JSON file relative to the current module
const dataUrl = new URL('./data.json', import.meta.url);
const data = await import(dataUrl, { with: { type: 'json' } });

Un ejemplo completo: widget de clima dinámico

El widget de clima cargará dinámicamente el módulo para obtener datos del clima solo cuando el usuario lo solicite. Este es un escenario ideal para las importaciones dinámicas, ya que retrasa la carga de código potencialmente pesado de interacción con API hasta que realmente se necesita.

El ejemplo utiliza tres archivos: una página HTML, un script de entrada y el módulo cargado de forma diferida.

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dynamic Weather Widget</title>
</head>
<body>
    <h1>Weather Widget</h1>
    <button id="loadWeather">Load Weather</button>
    <div id="weatherOutput">Click the button to load the weather.</div>

    <script src="index.js"></script>
</body>
</html>

index.js:

document.getElementById('loadWeather').addEventListener('click', async () => {
    const output = document.getElementById('weatherOutput');
    try {
        const weatherModule = await import('./weatherModule.js');
        const data = await weatherModule.loadWeather();
        output.textContent = `Weather: ${data.weather}`;
    } catch (err) {
        output.textContent = 'Failed to load weather data.';
    }
});

Este código activa una importación dinámica ante la interacción del usuario:

  1. Manejador de eventos: Adjunta un manejador de clic al botón.
  2. Importación dinámica: Usa await import() para obtener el módulo solo cuando se hace clic, manteniendo pequeño el bundle inicial.
  3. Manejo de errores: El bloque try...catch envuelve tanto el import() como la llamada de datos, de modo que una descarga fallida o una solicitud rechazada muestra el mensaje de reserva.

Este enfoque ayuda a que las páginas web sean eficientes y respondan bien, cargando recursos solo cuando son necesarios y proporcionando retroalimentación inmediata ante las interacciones del usuario.

weatherModule.js:

export async function loadWeather() {
    // Simulated API call
    return new Promise(resolve => {
        setTimeout(() => {
            resolve({ weather: 'Sunny, 76°F' });  // Simulating weather data
        }, 1000);
    });
}

La función simula la obtención de datos desde una fuente remota sin necesitar una API real: se resuelve después de un retraso de un segundo para que puedas ver la carga diferida en acción.

Explicación del ejemplo

  • Configuración HTML: Proporciona un botón y un contenedor para la salida.
  • Importación dinámica en acción: Al hacer clic en el botón, index.js carga weatherModule.js bajo demanda.
  • Módulo de clima: Simula un retraso de API, mostrando cómo las importaciones dinámicas difieren la lógica pesada o condicional hasta que realmente se necesita.

Errores comunes

  • await fuera de un módulo o función async. El await import() de nivel superior solo funciona en módulos ES; en scripts simples o callbacks no asíncronos, usa .then().
  • Olvidar .default. La exportación predeterminada de un módulo se accede a través de la propiedad default del object resuelto, no del object en sí.
  • Rutas completamente dinámicas. Los bundlers no pueden dividir una ruta que no pueden analizar. Mantén la parte literal del especificador (directorio y extensión) estática.
  • Exceso de división. Cada fragmento dinámico es una solicitud independiente. Divide el código grande o poco utilizado, no cada pequeño helper.

Conclusión

import() dinámico permite cargar módulos bajo demanda, devolviendo una promesa que se resuelve con el object de espacio de nombres del módulo. Impulsa el code splitting, los componentes con lazy loading, la carga condicional y las importaciones según configuración regional, mejorando el rendimiento de inicio cuando se usa deliberadamente. Combínalo con async/await y un sólido manejo de errores, y apóyate en tu bundler para convertir cada import() en un fragmento optimizado.

Para profundizar más, revisa los módulos ES: export e import, la introducción a los módulos y las promesas.

Práctica

Práctica
¿Qué afirmaciones sobre JavaScript dynamic import() son correctas?
¿Qué afirmaciones sobre JavaScript dynamic import() son correctas?
Was this page helpful?