# Safari 27 让顶层 await 符合规范

> WebKit 用纯 C++ 重写了 Safari 的 ES 模块加载器，顶层 await 自此按 ECMAScript 规范执行，不再依赖 2016 年起就停更的旧提案。

2026 年 9 月 17 日，WebKit 发布了 Safari 27.0，其特性文章称这一版带来「全新的 ES 模块加
载器」：它是对 ECMAScript 规范自身模块加载算法的纯 C++ 实现，取代了此前那套基于 2016 年
就已被放弃的 WHATWG Loader 提案的实现；用 WebKit 的原话，那份提案「完全早于顶层
await」。这一版的「已修复问题」清单用一句话记下了用户可见的结果：「Fixed multiple
top-level await correctness bugs with a rewrite of the ES module loader for standards
compliance.」这不是开关、不是 polyfill，也不是一处定点补丁。旧的加载器被换成了纯 C++ 的重写实现；用那份重写拉取请求自己的说法，旧实现是「当前这套
C++ 与 JS 的混合体（amalgam of C++ and JS）」，依据的是一份十年前就已被放弃的提案。WebKit 把这次重写归入该文的「Foundations」
一节，旁边正是解释整套做法的那句话：「有时候提升质量
的最好办法就是从头再来。」Kai Tamkun 撰写的工程记录则早两周发布，时间是 2026 年 9 月
2 日。

## 这次改动到底改了什么

ECMAScript 规范并没有把模块加载从头到尾定义完。WebKit 的说法是，规范的模块章节把一部分
内容留给宿主环境，其中包括抓取模块的机制：在浏览器里走网络，在 Node.js、Bun 这类运行时
里则来自本地文件系统。Safari 需要为这块宿主定义的部分给出答案，而它当年选的答案是 WHATWG
Loader 提案：加载器最初编写时，WebKit 依赖的就是该提案对宿主侧功能的规定。WebKit 指出，
这份提案最后一次更新是在 2016 年 1 月。

以当时的条件看，这个选择是站得住脚的。按 WebKit 的描述，那时模块执行是纯同步的，模块顶层
的 `await` 还不存在。问题随 ECMAScript 2022 出现，这个版本引入了该特性。而到那时，用
WebKit 的原话说，WHATWG Loader 提案在被 ECMAScript 标准的模块章节取代之后已经基本湮没无
闻，但 Safari 的加载器仍然建立在它之上。于是这项特性是基于一份早于语言中任何 async/await
支持的提案实现的，而不是依照标准关于异步模块求值的算法。WebKit 把由此产生的若干隐蔽缺陷
归因于这种错配，并表示在多次尝试修补之后，这些缺陷多年都没能被彻底解决，团队最终决定重建
基础，而不是继续在旧地基上打补丁。

真正完成这件事的拉取请求是 WebKit/WebKit#57827，标题为「[JSC] Rewrite module loader」。
它于 2026 年 2 月 4 日提交，2026 年 4 月 14 日合并，审阅者为 Yusuke Suzuki 与 Sosuke
Suzuki。它的描述毫不含糊地写明了起点：旧加载器存在长期遗留缺陷，包括在合法 ES 模块上触发
断言失败、模块求值顺序错误，以及顶层 `await` 的种种怪异行为。该补丁移除了 C++ 与 JS 混杂
的实现，换成遵循现代 ECMAScript 规范（而非旧的 WHATWG Loader 提案）的纯 C++ 重写。具体
地说，`builtins/ModuleLoader.js` 被删除，取而代之出现了一批原生类型（
`CyclicModuleRecord`、`ModuleRegistryEntry`、`ModuleGraphLoadingState`、
`ModuleLoadingContext`），与规范自身描述的记录与状态机制相对应。

### 一个十行就能复现的问题

WebKit 这篇文章围绕一个复现案例展开：一个 `main.js` 并发地三次动态导入同一个模块，并打印
每次得到的命名空间的键；而被导入的那个模块以一个基于定时器的顶层 `await` 开头。在旧加载器
下，三次导入的完成顺序是 2、3、1，并且前两次以 `Cannot access 'someArray' before
initialization` 失败。在新加载器下，它们按 1、2、3 完成，每个命名空间的导出都是完整的。

WebKit 把这两种症状都追溯到同一个缺陷。当加载器第一次遇到那个带顶层 `await` 的模块时，它
开始执行该模块，并在 `await` 处暂停，把控制权交回给导入方；导入方随即发起第二次导入。按理
说，第二次导入的 promise 不应当在第一次导入求值完成之前兑现，但在旧加载器里它立即兑现了。
于是第二次导入在模块求值仍处于挂起状态时就「完成」了，读取它的导出便触碰到尚未初始化的绑
定，从而抛出异常。第三次导入遭遇的是同一件事。只有第一次导入成功打印出了键名，因为它在兑现时确实已经完成求值。

这并不是一个纯理论上的顺序细节。该行为于 2022 年 7 月 14 日被登记为 WebKit bug 242740，
标题是「[JSC] ReferenceError when multiple modules are simultaneously importing a module
containing a top-level await」。报告者指出：并发的导入中会有一个被拒绝；改成顺序导入则不
受影响；而在同一份精简复现上，Chrome 与 Firefox 都不会拒绝任何一个 promise。这份报告挂了
大约四年，如今状态为 RESOLVED FIXED，被这次重写的拉取请求关闭。

### 为什么重写要落到原生 C++

旧加载器是以自托管内建函数（self-hosted builtin）的形式用 JavaScript 写的。WebKit 很坦诚
地列出了这样做的真实好处：内建函数可以被内联进调用它的用户代码，省去跨越 JavaScript 与
C++ 边界的开销；而且从 JavaScript 创建对象更快，因为有时可以直接消除堆分配。

它列出的缺点才是最终胜出的一方。自托管代码启动更慢，因为它必须在运行时编译，而原生代码早
已提前编译完毕。内建函数天生具有很宽的使用特征，这让 JavaScriptCore 的优化 JIT 编译器更难
利用其使用模式。再者，模块加载器本来就不是热路径，运行时编译带来的收益本就很小。WebKit
给出的净结果是：整体性能不如 C++ 那样稳定和可预测，这正是这次重写彻底放弃自托管路线的
原因。

实现方法刻意地朴素。WebKit 表示，工作从 2026 年 1 月开始，第一步是删掉那个装着旧加载器的
整个 JavaScript 文件，然后把规范中的操作一个一个实现出来，把其中的伪代码翻译成 C++。由于
模块加载机制是一台复杂的状态机，为了给这项工作排序，团队通读规范、记录函数之间的调用关
系、画出一张流程图；像 `ExecuteModule` 和 `ModuleRequestsEqual` 这样不依赖其他函数的叶子
函数被最先实现。几周之后，新加载器已能处理最常见的情形，于是一份草稿拉取请求被提了上去。

### WebKit 如何确认重写是正确的

按文章所述，证据来自三个彼此独立的方向。Bun 的工程师（他们的运行时构建在
JavaScriptCore 之上，因此继承了同一批加载器问题）提供了他们收集到的、能体现错误行为的
测试用例；WebKit 把这些用例适配到 `jsc` 命令行 shell 并作为测试落地。接着团队写了一个
fuzzer，用它生成由模块（一部分带顶层 `await`，一部分不带）和 `import` 语句构成的复杂依赖
图，再把 JavaScriptCore 的输出与其他引擎对比，以逐字节一致作为通过条件；WebKit 报告说，
所有被测样例都得到了正确处理。最后，补丁一直压着没合，直到 test262 中所有与模块相关的测
试通过、WPT 中许多此前失败的模块测试被修好且没有引入回归。拉取请求自身新增的测试点明了
具体的失效形态：`concurrent-imports.js` 对应求值顺序，`sync-from-async.js` 对应重写之前
调试版本会触发的一处断言失败，`tla-cycle.js` 对应含顶层 `await` 的循环依赖，
`bare-resolution-failure.js` 对应解析失败的缓存。WebKit 补充说，在合并之前，团队已经用装
有新加载器的构建版本日常使用了数周，没有遇到问题。

## 对 PWA 开发者为什么重要

模块顶层的 `await` 是让模块直接在模块作用域里完成异步初始化的写法。MDN 把语义讲得很直
白：你可以在模块顶层、在 async 函数之外
单独使用 `await`；而如果一个模块的子模块使用了 `await`，这个模块会等待那些子模块执行完再
执行自己，同时并不阻塞其他子模块的加载。WebKit 对模块图的描述与此一致：当某个模块遇到顶
层 `await` 时，所有导入它的模块都会被挂起，直到该 `await` 兑现；而依赖图中不依赖它的兄弟
模块仍可并发执行。

来源所描述的触发条件很窄，也容易讲清楚：多个导入方在同一时刻去取同一个含有顶层 `await`
的模块。bug 242740 报告称，当多个模块同时抓取一个因顶层 `await` 而需要一些准备时间的导入
时，其中一个导入会被拒绝；而改成按顺序抓取这些模块时，这种情况不会发生。WebKit 自己的复现
就是把这一情形写了出来：同一个模块并发地三次导入那个带顶层 `await` 的模块。

这件事让开发者付出了什么，记录在这份缺陷报告里。报告者指出，在其附上的精简复现中，Chrome
与 Firefox 都不会出现这种拒绝，这说明该行为属于某一个引擎，而不是这项特性本身。同一份报告
后来的一条评论写道，不做大规模重构就很难绕开它，而且它并不是能用 polyfill 解决的东西；另
一条评论则指向 SvelteKit 仓库中的一个 issue，评论者称其他开发者在那里也撞上了同一个缺陷。
WebKit 还有一个更大的判断值得记下：加载器重建在正确的基础之上
以后，它把 ES 模块整体定位成在 Safari 中可以放心依赖的东西。这是关于加载器整体可靠性的陈
述，而不只是关于一个运算符。

时间上的问题不再是「等发布」，而是「支持底线定在哪」。WebKit 说明，Safari 27.0 随 macOS
27 Golden Gate、iOS 27、iPadOS 27 与 visionOS 27 一同提供，另外也可以在 macOS 26 Tahoe 与
macOS 15 Sequoia 上独立于系统升级安装。也就是说修复已经在正式发布的版本里；还要等的是你
自己的已装机量追上来。

WebKit 在那篇工程文章里邀请开发者下载 Safari Technology Preview 251 或 Safari 27 beta，试用
顶层 `await` 并反馈问题；27.0 就是可以做这项测试的正式版本。此前靠「把带顶层 `await` 的模块改
成按顺序导入、而不是并发导入」来回避缺陷的站点（这正是 bug 242740 报告者指出的、失败与不失败两
种情形之间的唯一差别），可以保留这一做法，直到自己的统计数据显示 27 以下的版本已经消退：正式发
布的修复移动的是支持底线，而不是今天的流量构成。

## 站内相关条目

- [平台](/zh/reference/platforms/)：这是某个平台 JavaScript 引擎层面的行为变更，而平台
  页正是 OpenPWA 记录「每个浏览器／操作系统组合实际上可以依赖什么」的地方。
- [Baseline 支持概览](/zh/reference/platforms/baseline-2026/)：WebKit 把这项特性的出现
  时间定在 ECMAScript 2022；这一页解释了 Baseline 的分档，以及 Baseline 追踪的那组固定浏
  览器；要读懂一次只落在单一引擎上的修复，正需要这个参照框架。
- [按浏览器查看兼容性](/zh/compatibility/by-browser/)：在你决定依赖它之前该查的那张
  表：它给出 PWA 能力在各浏览器家族上的支持状况，而这正是某个引擎正式发布修复之后你真正要问
  的问题。

## 尚未验证与存疑之处

- WebKit 列出了 Safari 27.0 随哪些系统版本一同提供、以及可以装在哪两个较早的 macOS 版本
  上，但这次加载器重写只是在「这个发布版本」的层面被描述的。来源并没有按操作系统版本或设备
  类别拆分行为；因此「每个 Apple 平台上的每个 Safari 27.0 构建是否都带着重写后的加载器」，
  是就该发布版本给出的论断，而不是本文逐平台核实过的事实。
- WebKit 对 ES 模块整体可靠性的论断，是厂商对自己工作的表述。它引用的测试证据（test262、
  WPT、fuzzer、Bun 提供的用例）具体且可核查，但「可以放心依赖」本身不是一项度量。
- fuzzer 的对比对象是「其他引擎」的输出，通过条件是逐字节一致。来源并没有列出具体是哪些引
  擎，也没有说明生成的依赖图规模有多大。
- 其他以 JavaScriptCore 为基础的嵌入方（最明显的就是提供了测试用例的 Bun）是否会继承
  这次重写、按什么时间表继承，WebKit 的这些来源都没有涉及。

## 参考来源

- [WebKit 博客，2026 年 9 月 2 日：顶层 `await` 修复背后的模块加载器重写，作者 Kai Tamkun](https://webkit.org/blog/18227/)（webkit.org）
- [WebKit Features for Safari 27.0，2026 年 9 月 17 日：带着新 ES 模块加载器的正式发布版本](https://webkit.org/blog/18325/webkit-features-for-safari-27-0/)（webkit.org）
- [[JSC] Rewrite module loader，WebKit/WebKit#57827](https://github.com/WebKit/WebKit/pull/57827)（github.com）
- [WebKit bug 242740：并发导入同一个含顶层 await 的模块时抛出 ReferenceError](https://bugs.webkit.org/show_bug.cgi?id=242740)（bugs.webkit.org）
- [Safari Technology Preview 251 发布说明](https://webkit.org/blog/18194/release-notes-for-safari-technology-preview-251/)（webkit.org）
- [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)（developer.mozilla.org）