JavaScript’te Top-Level Await: Modül Girişinde Asenkron Kurulumun İncelikleri
ES modüllerinde top-level await ile config/SDK yükleme, feature flag ve bootstrap akışını doğru kurgulama.
JavaScript’te çoğu asenkron iş async fonksiyonların içine hapsolur. Oysa ES modülleri sayesinde top-level await ile modülün en üst seviyesinde bekleyebilir, uygulamayı “hazır olana kadar” doğru şekilde başlatabilirsiniz.
Not: Top-level await yalnızca ES module ortamında çalışır (tarayıcıda
<script type="module">, Node.js’te ESM).
Ne zaman işe yarar?
Top-level await özellikle “uygulama başlamadan önce” şu işler gerektiğinde güzel bir araçtır:
- Uzak config (ör. CDN’den
config.json) yükleme - Feature flag veya A/B testi kuralları çekme
- WASM / i18n sözlüğü gibi ağır kaynakları yükleyip sonra modülü kullanma
- SDK başlatma (analytics, ödeme, harita) ve hazır olana kadar bekleme
Bu yaklaşım, “her yerde init().then(...)” yazmak yerine, bağımlılıkları modül sınırında netleştirir.
Temel kullanım
// config.mjs
export const config = await fetch('/config.json').then(r => r.json());
// app.mjs
import { config } from './config.mjs';
console.log('API:', config.apiBase);
Bu örnekte app.mjs, config.mjs tamamlanmadan çalışmaz. Yani modül grafiğinde “doğal bir bekleme” oluşur.
Gerçekçi örnek: Feature flag ile koşullu modül yükleme
Top-level await’ı dinamik import ile birleştirmek, bundle’ı ve başlangıç maliyetini yönetmek için iyi bir tekniktir.
// flags.mjs
export const flags = await fetch('/flags').then(r => r.json());
// bootstrap.mjs
import { flags } from './flags.mjs';
if (flags.newCheckout) {
const { startNewCheckout } = await import('./checkout-new.mjs');
startNewCheckout();
} else {
const { startLegacyCheckout } = await import('./checkout-legacy.mjs');
startLegacyCheckout();
}
Kazanç: Kullanıcıların bir kısmı hiç kullanmayacağı kodu indirmeyebilir.
Hata yönetimi: Başlangıçta patlamasın
Top-level await’da atılan hata, modül yüklenmesini başarısız yapar. Bu bazen istenir, bazen de “yumuşak düşüş” daha iyidir.
// safe-config.mjs
export const config = await (async () => {
try {
const r = await fetch('/config.json');
if (!r.ok) throw new Error('Config indirilemedi');
return await r.json();
} catch (e) {
// Güvenli varsayılanlar
return { apiBase: '/api', telemetry: false };
}
})();
Bu sayede uygulama, config servisi geçici olarak sorun yaşasa bile ayağa kalkabilir.
Dikkat: Bağımlılık zinciri ve “başlangıç gecikmesi”
Top-level await, modül grafiğinde bekleme yarattığı için yanlış yerde kullanılırsa başlangıcı gereksiz uzatır.
Öneriler:
- Sadece gerçekten başlangıç için gerekli işleri top-level await ile yapın.
- “UI hemen gelsin, veri sonra aksın” gibi senaryolarda asenkron işi içeride başlatıp suspense benzeri bir akış kurun.
- Uzun işler için timeout + fallback düşünün.
Basit timeout örneği:
const withTimeout = (p, ms) =>
Promise.race([
p,
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))
]);
export const config = await withTimeout(
fetch('/config.json').then(r => r.json()),
1500
).catch(() => ({ apiBase: '/api' }));
Node.js tarafı: ESM ve test edilebilirlik
Node’da ESM kullanıyorsanız (ör. "type": "module"), top-level await ile DB bağlantısı gibi işleri modül girişine koymak cazip gelebilir. Ancak testlerde “import edince bağlanıyor” durumu istenmeyebilir.
Pratik yaklaşım:
- Top-level await’ı salt veri/konfig gibi yan etkisi düşük şeylerde kullanın.
- Yan etkili işleri (DB connect, server listen) mümkünse bir
start()fonksiyonunda başlatın.
Sonuç
Top-level await, JavaScript modül sistemini daha ifade gücü yüksek hale getiriyor: bağımlılıkları “init zincirleri” yerine modül sınırında tanımlıyorsunuz. Doğru kullanıldığında kodu sadeleştirir; yanlış kullanıldığında ise başlangıcı yavaşlatabilir. Kural basit: kritik bootstrap için kullan, geri kalanını akış içinde yönet.