顶级 await 允许模块顶层暂停执行以等待 Promise 完成,但不改变静态依赖解析;它使 import 阻塞于求值阶段而非动态导入,仅限 ESM,Node.js 16+ 和主流浏览器支持。
顶级 await 不是让整个模块“等完再执行”,而是让 import 这个动作可以暂停并等待 Promise 完成 —— 模块本身仍按 ES 模块的静态依赖图解析,但初始化阶段(top-level execution)允许异步阻塞。
await
ES 模块的加载分两步:先静态分析 import 语句构建依赖图(此时不允许任何运行时逻辑),再执行模块体(此时才跑代码)。旧规范下,await 只能在 async function 内部出现,而模块顶层不是函数,所以直接写 await fetch(...) 会报 SyntaxError: await is only valid in async functions and the top level bodies of modules —— 注意后半句,它其实已经留了口子,只是早期引擎没实现。
常见错误现象:
.mjs 或 type="module" 脚本里写 const data = await fetch('/api').then(r => r.json()),报语法错误(async () => { ... })() 包一层来“模拟”,结果 export 变量变成 undefined(因为导出必须在模块执行期完成,而 IIFE 是异步延迟的)await 出现在模块顶层时,import 行为怎么变关键变化在于:当模块 A import 模块 B,而 B 使用了顶级 await,那么 A 的执行会被挂起,直到 B 中所有顶级 await 都 resolve 完成。这不是“动态导入”,而是模块实例化(instantiation)和求值(evaluation)两个阶段之间的等待。
使用场景:
await fs.readFile('./config.json'))再决定导出哪些 APIexport
注意点:
await 不会改变 import 的静态性 —— 你依然不能把 await 放在 if 分支里动态决定要不要 import
await,模块 A 即使没用 await,也会被阻塞;这是模块系统的同步等待,不是 JS 事件循环的等待await 的支持程度不一;Webpack 5+ 支持,但需开启 experiments.topLevelAwait: true
Node.js 自 14.8(实验)、16.0(默认启用)起支持顶级 await,且行为较统一;浏览器方面,Chrome 89+、Firefox 89+、Safari 15.4+ 支持,但有个关键限制:只在 type="module" 的 中有效,不支持在普通脚本或内联 script 中使用。
一个典型兼容写法示例(Node.js + ESM):
/* config.mjs */ const res = await fetch('https://api.example.com/config'); export const CONFIG = await res.json();
/ main.mjs / import { CONFIG } from './config.mjs'; console.log(CONFIG); // 确保这里拿到的是已解析的值,不是 Promise
容易踩的坑:
.cjs)文件中写顶级 await,Node.js 直接报错 —— 它只在 ESM 模块中合法await,需启用 @babel/plugin-syntax-top-level-await,且目标环境必须真实支持(Babel 不会 polyfill 运行时行为)await,它们的执行顺序由 import 图决定,不是按文件名或书写顺序;环形依赖中含顶级 await 会导致死锁真正复杂的地方在于:它看起来像“让模块变懒”,实则强化了模块间的强同步依赖 —— 一个模块的顶级 await 会让所有祖先模块的执行卡住,这点比 dynamic import() 更隐蔽,也更难调试。