top-level await в ECMAScriptДо появления top-level await оператор await
разрешалось использовать только внутри асинхронных функций. Любая
попытка написать асинхронный код на верхнем уровне модуля приводила к
синтаксической ошибке:
const response = await fetch('/api/data');
Для обхода ограничения использовались конструкции вида:
(async () => {
const response = await fetch('/api/data');
})();
или:
fetch('/api/data')
.then(response => response.json())
.then(data => {
console.log(data);
});
С внедрением top-level await ECMAScript-модули получили
возможность выполнять асинхронный код непосредственно на верхнем уровне
файла:
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
Это существенно изменило модель инициализации модулей и повлияло на архитектуру сборщиков, включая Webpack.
top-level awaitКогда модуль содержит await на верхнем уровне, он
становится асинхронным модулем.
Например:
// config.js
const response = await fetch('/config.json');
export const config = await response.json();
Любой модуль, импортирующий config.js, автоматически
начинает ожидать завершения его инициализации:
import { config } from './config.js';
console.log(config);
Webpack строит граф зависимостей с учетом асинхронности. Если хотя бы
один модуль в цепочке использует top-level await, все
зависимые модули становятся частью асинхронного execution graph.
top-level await в WebpackПолноценная поддержка появилась в Webpack 5.
Ранее использование await на верхнем уровне либо
полностью запрещалось, либо требовало экспериментальных флагов.
Базовая конфигурация:
module.exports = {
experiments: {
topLevelAwait: true
}
};
В современных версиях Webpack возможность часто включена автоматически при использовании ES-модулей, однако явное указание опции делает конфигурацию предсказуемой.
top-level await работает только в ECMAScript
Modules.
Корректные варианты:
import { api } from './api.js';
const data = await api.load();
или:
export const result = await fetch('/data');
Нельзя использовать top-level await внутри CommonJS:
const fs = require('fs');
const data = await readFile(); // ошибка
Webpack должен обрабатывать файл как ES Module.
Обычная цепочка:
index.js
├── app.js
└── utils.js
Все модули выполняются последовательно и синхронно.
При появлении top-level await:
index.js
└── config.js (await)
└── api.js
Webpack вынужден:
index.jsconfig.jsФактически runtime превращается в систему асинхронной загрузки модулей.
Исходный код:
const response = await fetch('/data');
export const data = await response.json();
Webpack преобразует во внутреннюю структуру, напоминающую:
__webpack_require__.a(module, async (__webpack_handle_async_dependencies__) => {
const response = await fetch('/data');
const data = await response.json();
__webpack_exports__.data = data;
});
Каждый асинхронный модуль получает специальную обертку.
Webpack формирует специальные async boundaries — точки, где выполнение может быть приостановлено.
Например:
import './config.js';
console.log('start');
Если config.js содержит top-level await,
то:
console.log('start')
не выполнится до завершения асинхронной инициализации.
top-level await// a.js
console.log('A');
// b.js
console.log('B');
// index.js
import './a';
import './b';
Результат:
A
B
top-level await// a.js
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('A');
// b.js
console.log('B');
Теперь порядок зависит от графа зависимостей и асинхронного состояния.
Если b.js зависит от a.js, выполнение будет
ожидать завершения таймера.
Один из наиболее распространённых сценариев:
export const config = await fetch('/config.json')
.then(r => r.json());
или:
const env = await loadEnvironment();
export default env;
Это позволяет:
Webpack активно использует top-level await при работе с
WebAssembly.
Пример:
import wasmModule from './math.wasm';
const instance = await wasmModule();
export const add = instance.exports.add;
Без top-level await приходилось строить сложные цепочки
промисов.
const firebase = await import('./firebase-init.js');
export default firebase;
или:
const analytics = await createAnalytics();
analytics.start();
Иногда требуется отложенная инициализация тяжелых компонентов:
const hugeLibrary = await import('./huge-library');
export default hugeLibrary;
Webpack может вынести такую зависимость в отдельный chunk.
top-level await хорошо интегрируется с dynamic
import.
Пример:
const page = await import('./pages/admin.js');
page.render();
Webpack создаст:
import()import()import('./module.js').then(module => {
module.start();
});
top-level awaitconst module = await import('./module.js');
module.start();
Код становится линейным и проще для чтения.
Неправильное использование top-level await может создать
последовательную загрузку.
Плохой пример:
const config = await loadConfig();
const user = await loadUser();
const settings = await loadSettings();
Каждая операция ожидает предыдущую.
Лучше:
const [config, user, settings] = await Promise.all([
loadConfig(),
loadUser(),
loadSettings()
]);
top-level await блокирует выполнение зависимых
модулей.
Если модуль долго инициализируется:
await new Promise(resolve => setTimeout(resolve, 5000));
вся цепочка импорта будет ожидать завершения.
Большое количество асинхронных модулей приводит к:
Пример:
App
├── Router
├── Store
│ └── Config (await)
└── UI
Пока Config не завершится, Store не будет
готов, а значит App тоже не сможет полностью
инициализироваться.
top-level await особенно опасен при циклических
импортов.
// a.js
import { valueB } from './b.js';
export const valueA = await getA(valueB);
// b.js
import { valueA } from './a.js';
export const valueB = await getB(valueA);
Это может привести к deadlock-сценарию.
Webpack пытается:
Но сложные циклические зависимости всё равно способны вызвать runtime errors.
async function init() {
await load();
}
init();
Модуль загружается сразу.
top-level awaitawait load();
Модуль считается незавершённым до окончания операции.
Разница фундаментальна:
| Подход | Блокирует импорт |
|---|---|
| async function | Нет |
| top-level await | Да |
Webpack позволяет писать:
await initializeApp();
import('./bootstrap');
или:
const config = await loadConfig();
start(config);
Entry module становится асинхронным.
Webpack 5 внедрил promise-based runtime для async modules.
Внутренне runtime:
Babel долгое время не имел полноценной поддержки
top-level await.
Современные версии используют:
{
"plugins": [
"@babel/plugin-syntax-top-level-await"
]
}
Важно понимать:
top-level awaitДля TypeScript требуется:
{
"compilerOptions": {
"module": "esnext",
"target": "es2017"
}
}
или выше.
Поддержка зависит от среды выполнения.
Поддерживаются:
Проблемы возникают в:
Webpack не может полностью полифилить семантику async modules.
Асинхронность усложняет анализ зависимостей.
Webpack должен учитывать:
Иногда tree shaking становится менее агрессивным.
В Module Federation top-level await особенно
полезен.
Пример:
const remote = await import('remoteApp/bootstrap');
или:
await __webpack_init_sharing__('default');
Асинхронная инициализация remote containers становится естественной частью execution flow.
top-level awaitВ server-side rendering использование требует осторожности.
Если серверный entrypoint содержит:
await connectDatabase();
рендеринг будет заблокирован до завершения подключения.
Возможны:
top-level await хорошо подходит для:
Плохие варианты использования:
Вместо:
const a = await loadA();
const b = await loadB();
лучше:
const [a, b] = await Promise.all([
loadA(),
loadB()
]);
Желательно ограничивать количество async modules.
Плохая архитектура:
App
├── Config (await)
├── Store (await)
├── Router (await)
├── UI (await)
└── Theme (await)
Лучше концентрировать асинхронность ближе к entrypoint.
Хороший паттерн:
// index.js
import('./bootstrap');
// bootstrap.js
const config = await loadConfig();
startApp(config);
Асинхронность локализуется в bootstrap-фазе.
При использовании top-level await полезно
анализировать:
Webpack DevTools могут показывать:
Hot Module Replacement становится сложнее.
Если модуль:
await initializeSomething();
то при hot reload требуется повторная асинхронная инициализация.
Это может приводить к:
sideEffectsАсинхронные модули с побочными эффектами требуют особого внимания.
Например:
await initializeGlobalState();
Webpack не сможет безопасно удалить такой модуль при tree shaking.
При persistent caching Webpack кэширует:
Но runtime-await всё равно выполняется заново при запуске приложения.
Если chunk содержит async module:
chunk.js
└── module-with-await.js
Webpack:
Типичные проблемы:
Uncaught (in promise)
Circular dependency detected
Module execution failed
top-level await превращает модульную систему из
синхронной в частично асинхронную.
Это влияет на:
Webpack 5 фактически внедрил полноценную поддержку асинхронных модулей на уровне всего bundler runtime.