Utilisation du Top-Level Await en JavaScript Moderne

Si vous avez déjà encapsulé du code asynchrone dans une expression de fonction immédiatement invoquée (IIFE) juste pour utiliser await
au niveau module, vous n’êtes pas seul. Avant ES2022, les développeurs JavaScript devaient contourner les obstacles pour gérer les opérations asynchrones lors de l’initialisation des modules. Le top-level await JavaScript change cela en permettant l’utilisation d’await
directement dans les modules ES sans wrapper de fonction async
.
Cet article explique comment le top-level await transforme l’exécution des modules, ses applications pratiques pour le chargement de configuration et les imports dynamiques, et les compromis critiques que vous devez comprendre—y compris le blocage d’exécution et les pièges des dépendances circulaires. Vous apprendrez quand utiliser cette fonctionnalité puissante et, tout aussi important, quand l’éviter.
Points Clés à Retenir
- Le top-level await permet l’utilisation d’
await
directement dans les modules ES sans encapsulage dans des fonctions async - L’exécution des modules devient asynchrone, bloquant les modules dépendants jusqu’à leur achèvement
- Idéal pour l’initialisation ponctuelle, le chargement de configuration et les imports conditionnels
- À éviter dans les bibliothèques et utilitaires pour ne pas bloquer les consommateurs en aval
- Nécessite des modules ES et un support d’environnement d’exécution moderne (Node.js 14.8+, ES2022)
Qu’est-ce que le Top-Level Await et Pourquoi a-t-il été Introduit ?
Le Problème qu’il Résout
Avant le top-level await, initialiser un module avec des données asynchrones nécessitait des contournements :
// Ancienne approche avec IIFE
let config;
(async () => {
config = await fetch('/api/config').then(r => r.json());
})();
// config pourrait être undefined lors de l'accès !
Ce pattern créait des problèmes de timing et rendait le code plus difficile à comprendre. Les modules ne pouvaient pas garantir que leurs dépendances asynchrones étaient prêtes avant d’exporter des valeurs.
La Solution ES2022
Le top-level await permet les expressions await
directement dans la portée du module :
// Approche moderne
const config = await fetch('/api/config').then(r => r.json());
export { config }; // Toujours défini lors de l'import
Cette fonctionnalité fonctionne exclusivement dans les modules ES—fichiers avec l’extension .mjs
, ou fichiers .js
dans des projets avec "type": "module"
dans package.json. Dans les navigateurs, les scripts doivent utiliser <script type="module">
.
Comment le Top-Level Await Change l’Exécution des Modules
Le Chargement des Modules Devient Asynchrone
Quand JavaScript rencontre await outside async function, cela change fondamentalement la façon dont ce module se charge :
- Phase d’Analyse : Le moteur valide la syntaxe et identifie les imports/exports
- Phase d’Instanciation : Les liaisons de module sont créées mais non évaluées
- Phase d’Évaluation : Le code s’exécute, se mettant en pause à chaque
await
// database.js
console.log('1. Démarrage de la connexion');
export const db = await connectDB();
console.log('2. Connexion prête');
// app.js
console.log('3. Démarrage de l\'app');
import { db } from './database.js';
console.log('4. Utilisation de la base de données');
// Ordre de sortie :
// 1. Démarrage de la connexion
// 3. Démarrage de l'app
// 2. Connexion prête
// 4. Utilisation de la base de données
L’Effet de Cascade
Les dépendances de modules créent une réaction en chaîne. Quand un module utilise le top-level await, chaque module qui l’importe—directement ou indirectement—attend son achèvement :
// 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'; // Attend toute la chaîne
Cas d’Usage Courants et Patterns
Chargement Dynamique de Modules
Le top-level await JavaScript excelle dans les imports conditionnels basés sur les conditions d’exécution :
// Charger le pilote de base de données selon l'environnement
const dbModule = await import(
process.env.DB_TYPE === 'postgres'
? './drivers/postgres.js'
: './drivers/mysql.js'
);
export const db = new dbModule.Database();
Configuration et Initialisation de Ressources
Parfait pour charger la configuration ou initialiser des ressources avant l’exécution du module :
// i18n.js
const locale = await detectUserLocale();
const translations = await import(`./locales/${locale}.js`);
export function t(key) {
return translations.default[key] || key;
}
Chargement de Modules WebAssembly
Simplifie l’initialisation WASM sans fonctions wrapper :
// crypto.js
const wasmModule = await WebAssembly.instantiateStreaming(
fetch('/crypto.wasm')
);
export const { encrypt, decrypt } = wasmModule.instance.exports;
Discover how at OpenReplay.com.
Limitations Critiques et Compromis
Modules ES Uniquement
Le top-level await a des exigences de contexte strictes :
// ❌ CommonJS - SyntaxError
const data = await fetchData();
// ❌ Script classique - SyntaxError
<script>
const data = await fetchData();
</script>
// ✅ Module ES
<script type="module">
const data = await fetchData();
</script>
Blocage d’Exécution
Chaque await
crée un point de synchronisation qui peut impacter le démarrage de l’application :
// slow-module.js
export const data = await fetch('/slow-endpoint'); // Délai de 5 secondes
// app.js
import { data } from './slow-module.js';
// Toute l'app attend 5 secondes avant que cette ligne s'exécute
Blocages de Dépendances Circulaires
Le top-level await rend les dépendances circulaires plus dangereuses :
// 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);
// Résultat : Blocage - les modules s'attendent mutuellement indéfiniment
Bonnes Pratiques pour l’Utilisation en Production
Quand Utiliser le Top-Level Await
- Initialisation ponctuelle : Connexions de base de données, clients API
- Chargement de configuration : Paramètres spécifiques à l’environnement
- Détection de fonctionnalités : Chargement conditionnel de polyfills
Quand l’Éviter
- Modules de bibliothèque : Ne jamais bloquer les consommateurs en aval
- Utilitaires fréquemment importés : Garder synchrone pour les performances
- Modules avec risque de dépendance circulaire : Utiliser des fonctions async à la place
Stratégies de Gestion d’Erreurs
Toujours gérer les échecs pour éviter les crashes de chargement de module :
// Pattern sécurisé avec fallback
export const config = await loadConfig().catch(err => {
console.error('Échec du chargement de config:', err);
return { defaultSettings: true };
});
// Alternative : Laisser le consommateur gérer les erreurs
export async function getConfig() {
return await loadConfig();
}
Support des Outils de Build et d’Environnement d’Exécution
L’outillage moderne gère le top-level await JavaScript avec des approches variées :
- Webpack 5+ : Support avec
experiments.topLevelAwait
- Vite : Support natif en développement et production
- Node.js 14.8+ : Support complet dans les modules ES
- TypeScript 3.8+ : Nécessite
module: "es2022"
ou supérieur
Pour les environnements legacy, considérez encapsuler la logique async dans des fonctions exportées plutôt que d’utiliser le top-level await.
Conclusion
Le top-level await transforme la façon dont nous écrivons l’initialisation asynchrone de modules en JavaScript, éliminant les contournements IIFE et rendant le code plus lisible. Cependant, sa puissance s’accompagne de responsabilités—le blocage d’exécution des modules et les problèmes potentiels de dépendances circulaires nécessitent une considération attentive.
Utilisez le top-level await pour l’initialisation spécifique à l’application et le chargement de configuration, mais gardez-le hors des bibliothèques partagées et des utilitaires. En comprenant à la fois ses capacités et ses contraintes, vous pouvez exploiter cette fonctionnalité pour écrire des modules JavaScript plus propres et plus maintenables tout en évitant les pièges qui accompagnent la mise en pause de l’exécution des modules.
FAQ
Non, le top-level await fonctionne uniquement dans les modules ES. Dans Node.js, utilisez des fichiers .mjs ou définissez type module dans package.json. Les modules CommonJS doivent continuer à utiliser des fonctions async ou des IIFE pour les opérations asynchrones.
Le top-level await en lui-même n'empêche pas le tree shaking, mais il peut affecter la division des bundles. Les bundlers peuvent grouper différemment les modules avec top-level await pour maintenir l'ordre d'exécution, créant potentiellement des chunks plus volumineux.
La plupart des test runners modernes supportent les modules ES avec top-level await. Pour Jest, activez le support ESM expérimental. Considérez mocker les dépendances async ou encapsuler l'initialisation dans des fonctions pour faciliter les tests.
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.