12k
All articles

Uso de Top-Level Await en JavaScript Moderno

El top-level await en módulos ES elimina los async IIFE y afecta la ejecución de módulos, importaciones dinámicas y dependencias circulares en JavaScript.

OpenReplay Team
OpenReplay Team
Uso de Top-Level Await en JavaScript Moderno

Si alguna vez has envuelto código asíncrono en una expresión de función inmediatamente invocada (IIFE) solo para usar await a nivel de módulo, no estás solo. Antes de ES2022, los desarrolladores de JavaScript tenían que hacer malabares para manejar operaciones asíncronas durante la inicialización de módulos. Top-level await JavaScript cambia esto al permitir await directamente en módulos ES sin necesidad de un wrapper de función async.

Este artículo explica cómo top-level await transforma la ejecución de módulos, sus aplicaciones prácticas para la carga de configuraciones e importaciones dinámicas, y las compensaciones críticas que necesitas entender—incluyendo el bloqueo de ejecución y las trampas de dependencias circulares. Aprenderás cuándo usar esta poderosa característica y, igualmente importante, cuándo evitarla.

Puntos Clave

  • Top-level await permite await directamente en módulos ES sin envolver en funciones async
  • La ejecución del módulo se vuelve asíncrona, bloqueando módulos dependientes hasta completarse
  • Más adecuado para inicialización única, carga de configuraciones e importaciones condicionales
  • Evitar en bibliotecas y utilidades para prevenir bloquear consumidores posteriores
  • Requiere módulos ES y soporte de runtime moderno (Node.js 14.8+, ES2022)

¿Qué Es Top-Level Await y Por Qué Se Introdujo?

El Problema Que Resuelve

Antes de top-level await, inicializar un módulo con datos asíncronos requería soluciones alternativas:

// Enfoque antiguo con IIFE
let config;
(async () => {
  config = await fetch('/api/config').then(r => r.json());
})();

// ¡config podría ser undefined cuando se accede!

Este patrón creaba problemas de temporización y hacía el código más difícil de razonar. Los módulos no podían garantizar que sus dependencias asíncronas estuvieran listas antes de exportar valores.

La Solución ES2022

Top-level await permite expresiones await directamente en el ámbito del módulo:

// Enfoque moderno
const config = await fetch('/api/config').then(r => r.json());
export { config }; // Siempre definido cuando se importa

Esta característica funciona exclusivamente en módulos ES—archivos con extensión .mjs, o archivos .js en proyectos con "type": "module" en package.json. En navegadores, los scripts deben usar <script type="module">.

Cómo Top-Level Await Cambia la Ejecución de Módulos

La Carga de Módulos Se Vuelve Asíncrona

Cuando JavaScript encuentra await fuera de función async, cambia fundamentalmente cómo se carga ese módulo:

  1. Fase de Análisis: El motor valida la sintaxis e identifica imports/exports
  2. Fase de Instanciación: Se crean los enlaces del módulo pero no se evalúan
  3. Fase de Evaluación: El código se ejecuta, pausando en cada await
// database.js
console.log('1. Iniciando conexión');
export const db = await connectDB();
console.log('2. Conexión lista');

// app.js
console.log('3. App iniciando');
import { db } from './database.js';
console.log('4. Usando base de datos');

// Orden de salida:
// 1. Iniciando conexión
// 3. App iniciando
// 2. Conexión lista
// 4. Usando base de datos

El Efecto Cascada

Las dependencias de módulos crean una reacción en cadena. Cuando un módulo usa top-level await, cada módulo que lo importa—directa o indirectamente—espera su finalización:

// config.js
export const settings = await loadSettings();

// auth.js
import { settings } from './config.js';
export const apiKey = settings.apiKey;

// main.js
import { apiKey } from './auth.js'; // Espera toda la cadena

Casos de Uso Comunes y Patrones

Carga Dinámica de Módulos

Top-level await JavaScript sobresale en importaciones condicionales basadas en condiciones de tiempo de ejecución:

// Cargar driver de base de datos basado en el entorno
const dbModule = await import(
  process.env.DB_TYPE === 'postgres' 
    ? './drivers/postgres.js' 
    : './drivers/mysql.js'
);

export const db = new dbModule.Database();

Configuración e Inicialización de Recursos

Perfecto para cargar configuración o inicializar recursos antes de la ejecución del módulo:

// i18n.js
const locale = await detectUserLocale();
const translations = await import(`./locales/${locale}.js`);

export function t(key) {
  return translations.default[key] || key;
}

Carga de Módulos WebAssembly

Simplifica la inicialización de WASM sin funciones wrapper:

// crypto.js
const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('/crypto.wasm')
);

export const { encrypt, decrypt } = wasmModule.instance.exports;

Limitaciones Críticas y Compensaciones

Solo Módulos ES

Top-level await tiene requisitos de contexto estrictos:

// ❌ CommonJS - SyntaxError
const data = await fetchData();

// ❌ Script clásico - SyntaxError
<script>
  const data = await fetchData();
</script>

// ✅ Módulo ES
<script type="module">
  const data = await fetchData();
</script>

Bloqueo de Ejecución

Cada await crea un punto de sincronización que puede impactar el arranque de la aplicación:

// slow-module.js
export const data = await fetch('/slow-endpoint'); // Retraso de 5 segundos

// app.js
import { data } from './slow-module.js';
// Toda la app espera 5 segundos antes de que esta línea se ejecute

Bloqueos por Dependencias Circulares

Top-level await hace las dependencias circulares más peligrosas:

// user.js
import { getPermissions } from './permissions.js';
export const user = await fetchUser();

// permissions.js
import { user } from './user.js';
export const permissions = await getPermissions(user.id);

// Resultado: Bloqueo - los módulos se esperan mutuamente indefinidamente

Mejores Prácticas para Uso en Producción

Cuándo Usar Top-Level Await

  • Inicialización única: Conexiones de base de datos, clientes API
  • Carga de configuración: Configuraciones específicas del entorno
  • Detección de características: Carga condicional de polyfills

Cuándo Evitarlo

  • Módulos de biblioteca: Nunca bloquear consumidores posteriores
  • Utilidades importadas frecuentemente: Mantener síncronas para rendimiento
  • Módulos con riesgo de dependencia circular: Usar funciones async en su lugar

Estrategias de Manejo de Errores

Siempre manejar fallos para prevenir crashes en la carga de módulos:

// Patrón seguro con fallback
export const config = await loadConfig().catch(err => {
  console.error('Falló la carga de config:', err);
  return { defaultSettings: true };
});

// Alternativa: Dejar que el consumidor maneje errores
export async function getConfig() {
  return await loadConfig();
}

Soporte de Herramientas de Build y Runtime

Las herramientas modernas manejan top-level await JavaScript con enfoques variados:

  • Webpack 5+: Soporta con experiments.topLevelAwait
  • Vite: Soporte nativo en desarrollo y producción
  • Node.js 14.8+: Soporte completo en módulos ES
  • TypeScript 3.8+: Requiere module: "es2022" o superior

Para entornos legacy, considera envolver la lógica async en funciones exportadas en lugar de usar top-level await.

Conclusión

Top-level await transforma cómo escribimos inicialización asíncrona de módulos en JavaScript, eliminando las soluciones alternativas con IIFE y haciendo el código más legible. Sin embargo, su poder viene con responsabilidad—el bloqueo de ejecución de módulos y problemas potenciales de dependencias circulares requieren consideración cuidadosa.

Usa top-level await para inicialización específica de aplicaciones y carga de configuraciones, pero manténlo fuera de bibliotecas compartidas y utilidades. Al entender tanto sus capacidades como sus limitaciones, puedes aprovechar esta característica para escribir módulos JavaScript más limpios y mantenibles mientras evitas las trampas que vienen con pausar la ejecución de módulos.

Preguntas Frecuentes

¿Puedo usar top-level await en módulos CommonJS de Node.js?

No, top-level await solo funciona en módulos ES. En Node.js, usa archivos .mjs o establece type module en package.json. Los módulos CommonJS deben continuar usando funciones async o IIFEs para operaciones asíncronas.

¿Afecta top-level await al tree shaking y el tamaño del bundle?

Top-level await en sí mismo no previene el tree shaking, pero puede afectar la división de bundles. Los bundlers pueden agrupar módulos con top-level await de manera diferente para mantener el orden de ejecución, potencialmente creando chunks más grandes.

¿Cómo pruebo módulos que usan top-level await?

La mayoría de los test runners modernos soportan módulos ES con top-level await. Para Jest, habilita el soporte experimental de ESM. Considera hacer mock de dependencias async o envolver la inicialización en funciones para facilitar las pruebas.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.