top-level await — возможность использовать оператор
await непосредственно на верхнем уровне ES-модуля без
необходимости оборачивать код в асинхронную функцию. Поддержка этой
конструкции появилась в ECMAScript 2022 и стала важной частью
современного JavaScript.
Webpack поддерживает обработку top-level await, начиная
с Webpack 5. Эта возможность особенно востребована при:
Пример стандартного использования:
const response = await fetch('/config.json');
const config = await response.json();
console.log(config);
Без top-level await подобный код приходилось помещать в
асинхронную функцию:
async function bootstrap() {
const response = await fetch('/config.json');
const config = await response.json();
console.log(config);
}
bootstrap();
Webpack 5 умеет анализировать и компилировать
top-level await внутри ES-модулей. При этом система сборки
автоматически преобразует граф зависимостей в асинхронный.
Минимальная конфигурация:
module.exports = {
experiments: {
topLevelAwait: true
}
};
В более поздних версиях Webpack возможность может быть включена по умолчанию, однако явное указание остаётся хорошей практикой для совместимости и читаемости конфигурации.
Когда Webpack обнаруживает await на верхнем уровне
модуля, происходят серьёзные изменения в механизме загрузки
зависимостей.
Обычный ES-модуль:
import { api } from './api';
console.log(api);
загружается синхронно.
Модуль с top-level await:
const response = await fetch('/api/data');
export const data = await response.json();
становится асинхронным. Любой модуль, импортирующий его, также начинает ожидать завершения выполнения.
Пример:
import { data } from './data';
console.log(data);
Webpack преобразует такой граф зависимостей в цепочку промисов.
Фактически:
import('./data').then(module => {
console.log(module.data);
});
создаётся автоматически на уровне runtime.
top-level await влияет не только на конкретный файл, но
и на все связанные модули.
Структура:
main.js
└── config.js
└── await fetch(...)
Если config.js содержит top-level await,
тогда:
config.js становится асинхронным;main.js тоже становится асинхронным;Это влияет:
const response = await fetch('/config.json');
export const config = await response.json();
import { config } from './config';
console.log(config.apiUrl);
Webpack гарантирует:
fetch;config;app.js.top-level await хорошо сочетается с
import().
const module = await import('./feature');
module.run();
Webpack создаёт отдельный chunk и загружает его асинхронно.
Одна из ключевых причин появления top-level await —
поддержка WebAssembly.
Пример:
import wasmModule from './math.wasm';
const instance = await wasmModule();
console.log(instance.exports.sum(1, 2));
Webpack может автоматически ожидать инициализации
.wasm-модуля.
Babel долгое время не поддерживал полноценную трансформацию
top-level await, поскольку конструкция требует изменений в
системе модулей, а не только синтаксического преобразования.
Для работы необходимы:
type: "module" либо ES-модули;Плагин:
npm install @babel/plugin-syntax-top-level-await
Конфигурация:
module.exports = {
plugins: [
'@babel/plugin-syntax-top-level-await'
]
};
Важно понимать различие:
plugin-syntax-top-level-await только распознаёт
синтаксис;top-level await работает только в ES-модулях.
Неправильно:
const data = await fetch('/api');
в CommonJS:
module.exports = {};
require('./module');
Правильно:
export {};
import './module.js';
Webpack может смешивать CommonJS и ESM, однако
top-level await применяется исключительно к ESM-графу.
top-level await способен замедлять старт приложения.
Причина — блокировка выполнения зависимых модулей до завершения асинхронной операции.
Проблемный пример:
const translations = await fetch('/translations');
Если загрузка занимает 2 секунды, всё приложение может ждать завершения.
Особенно опасна ситуация, когда корневой модуль содержит долгий
await.
await initializeDatabase();
Тогда:
Рекомендуется избегать длительных операций на верхнем уровне.
Плохой подход:
const hugeData = await fetch('/big.json');
Лучший вариант:
export async function loadData() {
const response = await fetch('/big.json');
return response.json();
}
Тогда загрузка станет ленивой и управляемой.
top-level await усложняет обработку циклических
импортов.
Пример:
import './b';
await initializeA();
import './a';
await initializeB();
Возможны:
Webpack пытается обнаруживать подобные проблемы, но сложные циклы всё равно могут приводить к нестабильному поведению.
Если top-level await завершается ошибкой:
const response = await fetch('/missing-file');
ошибка распространяется на весь модульный граф.
Webpack отклоняет соответствующий Promise.
Пример:
import('./app')
.catch(error => {
console.error(error);
});
Наиболее безопасный вариант:
let config = {};
try {
const response = await fetch('/config.json');
config = await response.json();
} catch (error) {
console.error(error);
}
top-level await часто применяется в entry-point.
await initializeAnalytics();
await initializeAuth();
import('./bootstrap');
Webpack преобразует entry в асинхронную точку входа.
Webpack умеет совмещать:
top-level await;Пример:
const chartModule = await import('./charts');
Chunk загружается только при необходимости.
Асинхронные модули сложнее оптимизировать.
Причины:
Иногда top-level await уменьшает эффективность tree
shaking.
В Module Federation top-level await особенно полезен
для:
Пример:
const remote = await import('remote/App');
Webpack корректно ожидает готовности удалённого контейнера.
На сервере top-level await используется для:
Пример:
const templates = await loadTemplates();
export default templates;
Однако длительные операции могут замедлять первый запрос.
Для корректной работы необходимы:
"type": "module" в package.json.Пример:
{
"type": "module"
}
const config = await loadConfig();
Преимущества:
Недостатки:
async function bootstrap() {
const config = await loadConfig();
}
bootstrap();
Преимущества:
Недостатки:
Плохо:
await fetch('/huge-data');
Лучше:
export function loadHugeData() {
return fetch('/huge-data');
}
Хорошая практика:
export async function initialize() {
await connectDatabase();
}
Проблемный вариант:
require('./module-with-await');
Лучше полностью перейти на ES-модули.
Особенно при наличии:
await something();
в обоих модулях.
Подходящие сценарии:
Неподходящие:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
experiments: {
topLevelAwait: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
};
src/
├── index.js
├── config.js
├── api.js
└── bootstrap.js
const response = await fetch('/config.json');
export default await response.json();
import config from './config';
console.log(config);
import('./bootstrap');
Webpack внедряет специальный runtime-код для:
Асинхронный модуль получает внутренний статус:
pending
fulfilled
rejected
Runtime гарантирует корректный порядок исполнения даже при сложных графах зависимостей.
top-level await поддерживается современными
браузерами:
При использовании старых браузеров требуется:
Полностью полифилить механизм невозможно, поскольку он связан с моделью модулей JavaScript.
Типичные ошибки:
Причины:
Причины:
Причина — циклические асинхронные импорты.
Наиболее оправданные сценарии:
Нежелательные сценарии:
В подобных случаях эффективнее: