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:
- Fase de Parsing: O engine valida sintaxe e identifica imports/exports
- Fase de Instanciação: Bindings do módulo são criados mas não avaliados
- 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;
Discover how at OpenReplay.com.
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.