Back

Usando Top-Level Await no JavaScript Moderno

Usando Top-Level Await no JavaScript Moderno

Introdução

Se você já encapsulou código assíncrono em uma expressão de função imediatamente invocada (IIFE) apenas para usar await no nível do módulo, você não está sozinho. Antes do ES2022, desenvolvedores JavaScript precisavam dar voltas para lidar com operações assíncronas durante a inicialização de módulos. Top-level await JavaScript muda isso ao permitir await diretamente em módulos ES sem um wrapper de função async.

Este artigo explica como o top-level await transforma a execução de módulos, suas aplicações práticas para carregamento de configuração e importações dinâmicas, e os trade-offs críticos que você precisa entender—incluindo bloqueio de execução e armadilhas de dependências circulares. Você aprenderá quando usar esse recurso poderoso e, igualmente importante, quando evitá-lo.

Principais Pontos

  • Top-level await permite await diretamente em módulos ES sem encapsular em funções async
  • A execução do módulo torna-se assíncrona, bloqueando módulos dependentes até a conclusão
  • Mais adequado para inicialização única, carregamento de configuração e importações condicionais
  • Evite em bibliotecas e utilitários para prevenir bloqueio de consumidores downstream
  • Requer módulos ES e suporte de runtime moderno (Node.js 14.8+, ES2022)

O Que É Top-Level Await e Por Que Foi Introduzido?

O Problema Que Resolve

Antes do top-level await, inicializar um módulo com dados assíncronos exigia soluções alternativas:

// Abordagem antiga com IIFE
let config;
(async () => {
  config = await fetch('/api/config').then(r => r.json());
})();

// config pode estar undefined quando acessado!

Esse padrão criava problemas de timing e tornava o código mais difícil de entender. Módulos não podiam garantir que suas dependências assíncronas estivessem prontas antes de exportar valores.

A Solução ES2022

Top-level await permite expressões await diretamente no escopo do módulo:

// Abordagem moderna
const config = await fetch('/api/config').then(r => r.json());
export { config }; // Sempre definido quando importado

Esse recurso funciona exclusivamente em módulos ES—arquivos com extensão .mjs, ou arquivos .js em projetos com "type": "module" no package.json. Em navegadores, scripts devem usar <script type="module">.

Como Top-Level Await Muda a Execução de Módulos

Carregamento de Módulos Torna-se Assíncrono

Quando JavaScript encontra await outside async function, isso muda fundamentalmente como aquele módulo carrega:

  1. Fase de Parsing: O engine valida sintaxe e identifica imports/exports
  2. Fase de Instanciação: Bindings do módulo são criados mas não avaliados
  3. Fase de Avaliação: Código executa, pausando a cada await
// database.js
console.log('1. Starting connection');
export const db = await connectDB();
console.log('2. Connection ready');

// app.js
console.log('3. App starting');
import { db } from './database.js';
console.log('4. Using database');

// Ordem de saída:
// 1. Starting connection
// 3. App starting
// 2. Connection ready
// 4. Using database

O Efeito Cascata

Dependências de módulos criam uma reação em cadeia. Quando um módulo usa top-level await, todo módulo que o importa—direta ou indiretamente—aguarda pela conclusão:

// 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'; // Aguarda toda a cadeia

Casos de Uso Comuns e Padrões

Carregamento Dinâmico de Módulos

Top-level await JavaScript se destaca em importações condicionais baseadas em condições de runtime:

// Carrega driver de banco baseado no ambiente
const dbModule = await import(
  process.env.DB_TYPE === 'postgres' 
    ? './drivers/postgres.js' 
    : './drivers/mysql.js'
);

export const db = new dbModule.Database();

Configuração e Inicialização de Recursos

Perfeito para carregar configuração ou inicializar recursos antes da execução do módulo:

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

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

Carregamento de Módulos WebAssembly

Simplifica inicialização WASM sem funções wrapper:

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

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

Limitações Críticas e Trade-offs

Apenas Módulos ES

Top-level await tem requisitos de contexto rigorosos:

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

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

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

Bloqueio de Execução

Cada await cria um ponto de sincronização que pode impactar a inicialização da aplicação:

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

// app.js
import { data } from './slow-module.js';
// Toda a aplicação aguarda 5 segundos antes desta linha executar

Deadlocks de Dependências Circulares

Top-level await torna dependências circulares mais perigosas:

// 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: Deadlock - módulos aguardam uns aos outros indefinidamente

Melhores Práticas para Uso em Produção

Quando Usar Top-Level Await

  • Inicialização única: Conexões de banco, clientes de API
  • Carregamento de configuração: Configurações específicas do ambiente
  • Detecção de recursos: Carregamento condicional de polyfills

Quando Evitar

  • Módulos de biblioteca: Nunca bloqueie consumidores downstream
  • Utilitários frequentemente importados: Mantenha síncronos para performance
  • Módulos com risco de dependência circular: Use funções async em vez disso

Estratégias de Tratamento de Erro

Sempre trate falhas para prevenir crashes no carregamento de módulos:

// Padrão seguro com fallback
export const config = await loadConfig().catch(err => {
  console.error('Config load failed:', err);
  return { defaultSettings: true };
});

// Alternativa: Deixe o consumidor tratar erros
export async function getConfig() {
  return await loadConfig();
}

Suporte de Ferramentas de Build e Runtime

Ferramentas modernas lidam com top-level await JavaScript com abordagens variadas:

  • Webpack 5+: Suporta com experiments.topLevelAwait
  • Vite: Suporte nativo em desenvolvimento e produção
  • Node.js 14.8+: Suporte completo em módulos ES
  • TypeScript 3.8+: Requer module: "es2022" ou superior

Para ambientes legados, considere encapsular lógica async em funções exportadas em vez de usar top-level await.

Conclusão

Top-level await transforma como escrevemos inicialização assíncrona de módulos em JavaScript, eliminando soluções alternativas com IIFE e tornando o código mais legível. No entanto, seu poder vem com responsabilidade—bloqueio de execução de módulos e potenciais problemas de dependências circulares requerem consideração cuidadosa.

Use top-level await para inicialização específica da aplicação e carregamento de configuração, mas mantenha-o fora de bibliotecas compartilhadas e utilitários. Ao entender tanto suas capacidades quanto suas limitações, você pode aproveitar esse recurso para escrever módulos JavaScript mais limpos e maintíveis, evitando as armadilhas que vêm com pausar a execução de módulos.

Perguntas Frequentes

Não, top-level await funciona apenas em módulos ES. No Node.js, use arquivos .mjs ou defina type module no package.json. Módulos CommonJS devem continuar usando funções async ou IIFEs para operações assíncronas.

Top-level await em si não impede tree shaking, mas pode afetar divisão de bundles. Bundlers podem agrupar módulos com top-level await de forma diferente para manter ordem de execução, potencialmente criando chunks maiores.

A maioria dos test runners modernos suportam módulos ES com top-level await. Para Jest, habilite suporte experimental ESM. Considere mockar dependências async ou encapsular inicialização em funções para facilitar testes.

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.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay