Що таке "top-level await"?
Що було раніше
До top-level await використовувати await можна було лише всередині async-функції:
// Помилка раніше
const data = await fetch('/api/data');
// Працює
async function main() {
const data = await fetch('/api/data');
console.log(await data.json());
}
main();Якщо потрібно було дочекатися чогось на початку модуля, доводилося створювати функцію-обгортку (init(), bootstrap() тощо).
Що змінилося
З top-level await тепер можна писати:
// module.mjs або type: "module"
const response = await fetch('https://api.example.com/user');
const user = await response.json();
console.log('Користувач:', user.name);Код призупиняє завантаження модуля, поки проміс не виконається. Це робить модуль асинхронним "за своєю природою".
Де можна використовувати
Лише в ES-модулях (ESM) - тобто у файлах:
- з розширенням
.mjs, або - у
.js, якщо вpackage.jsonвказано"type": "module", або - у
<script type="module">в браузері.
У CommonJS (Node.js require) - не можна.
Як це працює під капотом
Коли збирач або рантайм зустрічає await на верхньому рівні модуля:
- Модуль стає асинхронним.
- Його виконання призупиняється, поки не виконається
await. - Інші модулі, які імпортують цей модуль, теж чекають, поки він завершить виконання (через ланцюжок залежностей).
Приклад 1 - імпорт з очікуванням
// user.mjs
export const user = await fetch('/api/user').then(r => r.json());// main.mjs
import { user } from './user.mjs';
console.log("Ім'я користувача:", user.name);Тут main.mjs не почне виконуватися,
поки user.mjs не виконає свій await.
Приклад 2 - динамічна ініціалізація
// config.mjs
const env = await fetch('/env.json').then(r => r.json());
export const API_URL = env.production ? 'https://api.prod' : 'https://api.dev';Модуль config тепер можна імпортувати як звичайний,
а він сам асинхронно підтягне конфіг.
Приклад 3 - top-level await з динамічним імпортом
// main.mjs
const lang = navigator.language.startsWith('fr') ? 'fr' : 'en';
const messages = await import(`./messages.${lang}.js`);
console.log(messages.default.hello);Дозволяє динамічно імпортувати модуль, використовуючи асинхронний синтаксис без зайвих обгорток.
Взаємодія з іншими модулями
Якщо модуль A використовує await на верхньому рівні,
а модуль B імпортує A, то B теж чекає, поки A завершиться.
// a.mjs
console.log('A start');
await new Promise(r => setTimeout(r, 1000));
console.log('A done');
// b.mjs
import './a.mjs';
console.log('B start');Вивід:
A start
A done
B startПереваги
Спрощує ініціалізацію модулів (немає async init() навколо).
Усуває "піраміду імпортів" для асинхронних залежностей.
Дозволяє гнучко робити асинхронні імпорти (await import()).
Потенційні мінуси
Блокування ланцюжка модулів - якщо один модуль "висить" на довгому await,
усе дерево залежностей чекає на його завершення.
Не можна використовувати в CommonJS (лише ESM).
Може вплинути на час старту застосунку (особливо в Node JS).
Приклад у Node.js
// package.json
{
"type": "module"
}// index.js
import fs from 'fs/promises';
const config = JSON.parse(await fs.readFile('./config.json', 'utf-8'));
console.log('Config:', config);Працює, тому що Node JS підтримує top-level await в ESM-режимі.
Підсумок
| Особливість | Опис |
|---|---|
| Що робить | Дозволяє використовувати await поза async-функціями |
| Де працює | Лише в ES-модулях |
| Що повертає | Модуль стає асинхронним (повертає проміс при імпорті) |
| Застосування | Асинхронна ініціалізація, динамічні імпорти |
| Ризики | Блокування завантаження модулів, не можна в CommonJS |
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.