顶层 await 不阻塞页面渲染,仅阻塞模块执行;需配合 css 隐藏+js 移除 class 实现“等数据再显示”,且必须注意执行顺序、错误处理与 ssr 限制。

顶层 await 不能阻止页面渲染,它只阻塞模块执行,不阻塞 HTML 解析和 DOM 绘制。 你写 await fetch('/api') 在 <script type="module"></script> 里,浏览器照样会把 里的 HTML 解析完、样式计算完、甚至首帧都画出来了——数据没回来,内容就先裸奔。
为什么加了 type="module" 和顶层 await 还是看到空白页闪一下
这是最常见的误解:以为“模块 + await” = “页面等我”。实际机制是:
- HTML 解析和 DOM 构建全程异步且不受模块 await 影响
-
<script type="module"></script>默认defer,但它只控制脚本执行时机,不干预渲染流水线 - 如果
<main></main>或<div id="app"> 已在 HTML 中存在,它们就会被正常解析、布局、绘制——哪怕模块还没执行完 <li>没有 CSS 隐藏策略时,用户看到的就是未填充的 DOM 结构(比如空列表、缺头像的卡片)</li> <h3>怎么让页面真正“等数据好了再显示”</h3> <p>靠的是显式控制可见性,不是靠 await 拦住浏览器。三步必须闭环:</p> <ul> <li>在 <code>上初始加一个 class,比如loading: - CSS 隐藏关键区域(别用
display: none,它破坏布局流):body.loading main, body.loading .app { opacity: 0; visibility: hidden; pointer-events: none; } - 模块脚本中顶层 await 完成后,移除 class:
document.body.classList.remove('loading');
注意:这个 remove 必须发生在模块顶层(非函数内),且该模块必须是页面中第一个 <script type="module"></script>,否则可能被其他脚本抢跑。
容易踩的坑:CSS 和 JS 协作失效
很多翻车不是因为 await 写错了,而是隐藏逻辑没对齐:
- 如果模块脚本放在
底部,而内联 JS 提前读取document.body并操作 class,classList.remove可能报Cannot read property 'classList' of null - CSS 选择器权重不够,被后续样式覆盖,导致
opacity: 0不生效 - 没加
visibility: hidden,仅靠opacity: 0,元素仍响应点击或焦点(pointer-events: none是补救) - 服务端渲染(SSR)场景下,这个方案完全无效——顶层 await 只在客户端运行,服务端根本不会执行它
替代方案:普通 script 里怎么模拟顶层 await 效果
如果你不能改 type="module"(比如 CMS 插入脚本、老项目兼容),就用 async IIFE:
async function() {
try {
const res = await fetch('/api/config');
const config = await res.json();
// 初始化逻辑
document.body.classList.remove('loading');
} catch (err) {
console.error('init failed:', err);
document.body.classList.add('error');
}
}();
这个写法在普通 <script></script> 里合法,语义接近顶层 await,但要注意:它不阻塞模块图加载,也不影响其他脚本执行顺序——你得自己确保它最先执行。
最常被忽略的一点:顶层 await 的错误处理是静默失败。没包 try/catch,模块加载直接中断,页面白屏,控制台只有一行 Uncaught (in promise)。它不像函数调用那样能兜底,一旦出错,整个模块初始化就断了。











