Apple's browser engine team published a technical post on September 2, 2026 describing a ground-up rewrite of the component that loads JavaScript modules in Safari. The old one was built on a specification proposal that stopped being maintained in 2016, and the replacement, written in native C++, ships in Safari 27.
In short
Safari has carried a long-standing defect in which JavaScript modules that pause at the top level could finish loading out of order and hand back values that had not finished initializing. WebKit fixed it by deleting the old module loader outright and writing a new one in C++ that follows the current ECMAScript standard. The errors that showed up only in Safari stop once Safari 27 reaches users, and the fix is testable now in Safari Technology Preview 251 and the Safari 27 beta.
A loader built on a proposal that was abandoned
The announcement came from Kai Tamkun and was published on the WebKit blog on September 2, 2026. According to the post, WebKit for Safari 27 adds full specification compliance for top-level await, and the team fixed the problem at its root rather than patching around it. The framing is unusually direct for a browser release note: before this change, developers ran into unexpected errors reporting that a value had been accessed before initialization.
The cause sits in a division of labour written into the language standard itself. The ECMAScript specification's section on modules deliberately leaves some parts of the machinery to the host environment. Fetching a module is the clearest example. Web browsers pull modules over the network; JavaScript runtimes such as Node.js and Bun read them off the local filesystem. The standard declines to pick one, and each host fills in the gap.
WebKit filled that gap a long time ago. According to the post, Safari's module loader was written during the era of the WHATWG Loader proposal, a document last updated in January 2016, and the team relied on that proposal's specification of host-defined functionality. That worked in the early period of ES modules, when module execution was purely synchronous and top-level await did not exist as a language feature.
Then ECMAScript 2022 introduced it. By that point the WHATWG Loader proposal had faded into obscurity, superseded by the module section of the ECMAScript standard itself, but Safari's loader was still built on it. According to WebKit, top-level await ended up implemented in terms of a proposal that had been abandoned before any support for async and await existed in ECMAScript, rather than in accordance with the standard's algorithms for asynchronous module execution.
The mismatch produced what the post calls subtle bugs that could not be fully resolved for years, despite multiple attempts. Rather than continue patching a foundation that could not support the feature, the team decided to rebuild it.
What top-level await actually does
The feature is small in surface area and large in consequence. Top-level await allows the await keyword at the top level of a module, giving module authors the same handling of Promises that async functions already provide inside function bodies. Async functions let await expressions collapse complex Promise chains into linear sequences of code; top-level await extends that convenience to module scope.
The execution semantics differ in an important way between the two cases. Inside an async function, when an await expression is encountered, execution pauses until the awaited value is available, and control returns to the caller. For ES modules, according to the WebKit post, the analogy shifts: when a module hits a top-level await, any module that imports it is suspended until the await resolves. Sibling modules in the dependency graph that do not depend on the awaiting module can still execute concurrently.
That last clause is where the ordering guarantees live, and it is where Safari diverged from every other engine.
The ordering bug, demonstrated
WebKit's post includes a reproducible test case rather than a description. A main module dynamically imports the same module three times in sequence and prints the names of that module's exports after each import completes. The imported module opens with an await on a Promise resolved by a ten millisecond timeout, then declares an exported function and an exported array.
The expected output order is one, two, three. Under the old module loader, according to the post, the imports completed in the order two, three, one. The second and third attempts failed with an exception reading "Cannot access 'someArray' before initialization." Only the first import printed both export names successfully.
Two things went wrong, and both trace to a single defect. When the loader began loading the module containing top-level await for the first time, it started executing, then paused at the await and returned control to the main module, which began the second import. The promise for that second import was not supposed to resolve until the first import had finished evaluating. Because of the bug, it resolved immediately. The second import therefore finished first, while evaluation of the imported module was still incomplete, so the code printing the module's keys read uninitialized exports and threw. The third import failed the same way. Only after both had failed did the first import complete, at which point evaluation had finished and the keys printed correctly.
With the new module loader, according to WebKit, the three imports complete in order and each prints both export names.
Self-hosted builtins against native code
The rewrite changed the implementation language as well as the algorithm, and the post sets out the trade-off in some detail.
The old module loader was written in JavaScript as a self-hosted builtin. That approach carries real advantages. Unlike native code, JavaScript builtins can be inlined into the user code that invokes them. The performance penalty paid when crossing the boundary between JavaScript and C++ is avoided entirely. Object creation is faster from JavaScript, since it is sometimes possible to eliminate the heap allocation.
The drawbacks emerged later. Self-hosted JavaScript starts up more slowly because it has to be compiled at run time, whereas native code is fully compiled well in advance. Builtins have inherently wide usage characteristics, which makes it harder for JavaScriptCore's optimizing JIT compilers to exploit patterns in usage. And the module loader code, according to the post, is not a hot path, which further reduces the benefit of compiling at runtime. Overall performance turns out to be less stable and predictable than the equivalent C++.
That reasoning explains a decision that would otherwise look like a rewrite for its own sake. The team dropped the self-hosted builtin approach in favour of fully native code.
Deleting the file and rebuilding from the specification
Work began in January 2026. According to WebKit, because the consensus at that point held that self-hosted JavaScript was not the right approach, the process started by deleting the entire JavaScript file containing the old module loader.
What followed was a translation exercise. The team implemented the module loader operations one by one, converting the pseudocode defined in the ECMAScript specification into C++. Module loading machinery is a complex state machine, so the ordering of that work mattered. According to the post, the engineers read through the specification, noted how the functions called one another, and assembled a flow graph to establish a starting point.
Leaf functions were the natural place to begin, since they carry no dependencies on other functions. The post names ExecuteModule and ModuleRequestsEqual as two implemented early. After a few weeks, the new loader handled the most common cases and a draft pull request went up for review. WebKit is developed as open source, so that work sits in a public repository rather than behind an internal review process.
Testing against other engines
Reliability was the point of the exercise, which put unusual weight on the test strategy.
Engineers at Bun contributed test cases. Bun's runtime is built on JavaScriptCore and inherited the old module loader's problems directly, so the project had already collected examples demonstrating incorrect behaviour. According to WebKit, adapting those to run under JavaScriptCore's command line shell, jsc, and integrating them as tests was simple.
The second method was generative. The team wrote a fuzzer producing complex graphs of modules, some containing top-level await and some not, connected by import statements. The objective was to confirm that the observable effects of the module loading process were correct. Large graphs were generated, and the output of JavaScriptCore running the new loader was compared against the output of other JavaScript engines. Where the text output matched byte for byte, the case was handled correctly. According to the post, every tested example produced a match.
Two conformance suites gated the merge. All module-related tests from test262, the official ECMAScript conformance suite, had to pass. Many previously failing module tests from Web Platform Tests were fixed as well, with no regressions recorded. By the time the change merged, the team had been running a build of Safari with the new loader as a daily driver for several weeks without incident.
Where this sits in Safari's release cycle
The module loader is the third engine-level change to reach public view in Safari 27's development cycle this year, and the three are not obviously related to one another.
Safari 27 first appeared as a developer beta in June 2026. A source-code review of that beta by analytics engineer Mariusz Brucki, published in July, found that Safari 27 terminates connections to Bing and LinkedIn advertising endpoints by IP address at the network transport layer, before tracking data leaves the device. Separately, on July 1, 2026, WebKit shipped a Model Context Protocol server exposing seventeen tools to AI coding agents through Safari Technology Preview 247.
JavaScript engine fixes have surfaced in Technology Preview builds before without attracting much attention outside the specialist press. Safari Technology Preview 192, released in April 2024, carried corrections to Object.groupBy and Array.fromAsync alongside CSS changes. What distinguishes the current post is that it documents a rewrite rather than a patch, and states plainly that the previous architecture could not be made correct.
Why the module loader matters to marketing technology
Measurement libraries, consent management platforms and tag containers are all JavaScript that executes in the visitor's browser. When the browser's module machinery resolves imports in an order the specification does not sanction, the resulting failure is intermittent, browser-specific and dependent on timing. That combination is among the hardest to reproduce in a bug report and among the easiest to misattribute to a vendor's code.
The specific failure mode described by WebKit produces an exception rather than a silently wrong value, which cuts in two directions. An exception is visible, which makes it easier to detect than a quietly corrupted measurement. An exception also terminates the rest of the script, which in a consent or tagging context can leave a page in a partially configured state.
Module ordering is not primarily a question of latency, though the two intersect. A module suspended on a top-level await holds up every module importing it, and the loader determines which sibling work proceeds in parallel meanwhile. Getting that dependency graph wrong wastes time as well as producing incorrect results.
Browser-level performance differences have narrowed considerably over the past year. PPC Land reported in January 2026 that Safari 26.2, Firefox 146 and Chrome 143 closed the remaining gaps in measuring Largest Contentful Paint and Interaction to Next Paint, ending a period in which Core Web Vitals could only be measured reliably on Chrome. Commercial pressure on the same metrics has been building in parallel: in March 2026 Google AdSense emailed publishers recommending bfcache, the Speculation Rules API and AI-assisted debugging through Chrome DevTools, citing page view and revenue figures from Netzwelt and Yahoo! JAPAN News.
Technical baselines for publisher sites have also become more explicit. Joost de Valk's website specification, published in May 2026, sets 128 rules covering performance, structured data and crawler access, with Core Web Vitals targets listed as required rather than recommended. Against that background, a browser engine that cannot execute a standard language feature correctly represents a floor problem rather than an optimisation problem.
Availability and what remains open
Safari Technology Preview 251 and the Safari 27 beta both carry the new module loader and are available for download now. According to WebKit, the improvements extend past top-level await: with the loader rebuilt on the correct foundation, ES modules as a whole become more dependable in Safari. Once Safari 27 ships, the feature becomes available in production for all users.
WebKit has invited feedback and directs bug reports to bugs.webkit.org. No date has been published for the Safari 27 stable release, which historically arrives alongside the corresponding iOS and macOS releases in the autumn. The post that followed this one on the WebKit blog opens a call for Interop 2027 proposals, the annual cross-vendor process through which browser makers agree which web platform features to prioritise for compatibility work.
Timeline
- January 2016: The WHATWG Loader proposal receives its final update; Safari's module loader is built on its specification of host-defined functionality
- 2022: ECMAScript 2022 is released, introducing top-level await as a language feature, by which point the WHATWG Loader proposal has been superseded by the ECMAScript standard's own module section
- April 2024: Safari Technology Preview 192 ships fixes to Object.groupBy and Array.fromAsync alongside CSS and SVG changes
- September 2025: Safari 26 ships with iOS 26 and macOS 26
- December 2025: Safari 26.2, Firefox 146 and Chrome 143 complete cross-browser parity on Core Web Vitals measurement APIs
- January 2026: WebKit begins the module loader rewrite by deleting the JavaScript file containing the old implementation
- March 24, 2026: Google AdSense emails publishers recommending bfcache, Speculation Rules and Chrome DevTools MCP debugging
- May 2026: Joost de Valk publishes a 128-rule website specification with Core Web Vitals targets listed as required
- June 2026: Safari 27 becomes available as a developer beta
- July 1, 2026: WebKit publishes the Safari MCP server with seventeen tools in Safari Technology Preview 247
- July 2026: A source-code review of the Safari 27 beta documents network-layer IP blocking of Bing and LinkedIn advertising endpoints
- September 2, 2026: WebKit publishes Kai Tamkun's post describing the rewrite; the new module loader is available in Safari Technology Preview 251 and the Safari 27 beta
Related PPC Land coverage
- WebKit ships Safari MCP server with 17 tools for AI debugging agents - Covers the July 1, 2026 Model Context Protocol server built into Safari Technology Preview 247, another engine-level addition in the Safari 27 cycle.
- Safari 27 blocks LinkedIn and Bing ad trackers by IP address - Documents the network transport layer blocking mechanism found in the Safari 27 developer beta by analytics engineer Mariusz Brucki.
- Major browsers achieve cross-platform parity on web performance metrics - Reports the December 2025 releases that let Core Web Vitals be measured consistently across Safari, Firefox and Chrome.
- Safari Technology Preview 192: what marketing professionals should know - An earlier round of JavaScript engine corrections shipped through the Technology Preview channel.
- Google AdSense pushes bfcache, speculation rules, and AI debugging to publishers - Google's March 2026 guidance tying browser-level performance techniques directly to publisher advertising revenue.
- Safari 18.4 release brings major web platform enhancements - A prior WebKit feature release covering Declarative Web Push and CSS additions, useful for comparing how the team frames additions against corrections.
- WebKit introduces Digital Credentials API for Safari 26 - The October 2025 announcement of W3C Digital Credentials support, an example of WebKit implementing a standard rather than rebuilding one.
- Joost de Valk's website spec: 128 rules to future-proof your site - Sets out the technical baseline publishers are being measured against, including required Core Web Vitals thresholds.
Summary
Who: WebKit, Apple's browser engine team, published the work under the byline of engineer Kai Tamkun. Engineers at Bun, whose JavaScript runtime is built on JavaScriptCore and inherited the same defect, contributed test cases. The change affects web developers, publishers and marketing technology vendors whose code runs as ES modules in Safari.
What: A ground-up rewrite of Safari's JavaScript module loader, replacing an implementation written in JavaScript as a self-hosted builtin and based on the abandoned WHATWG Loader proposal with a native C++ implementation translated directly from the ECMAScript specification. The rewrite delivers full specification compliance for top-level await and corrects import ordering and premature promise resolution, the faults behind errors reporting access to values before initialization.
When: The rewrite began in January 2026 and was announced on September 2, 2026. It is available now in Safari Technology Preview 251 and the Safari 27 beta, and reaches general availability when Safari 27 ships.
Where: In WebKit, the engine behind Safari on macOS, iOS, iPadOS and visionOS, and in JavaScriptCore, the JavaScript engine WebKit shares with the Bun runtime.
Why: Safari's module loader was built on host-defined semantics from a proposal last updated in January 2016 and abandoned before async and await entered ECMAScript. Top-level await, introduced in ECMAScript 2022, was implemented against that obsolete foundation rather than the standard's algorithms for asynchronous module execution. According to WebKit, the resulting bugs resisted multiple attempts at repair over several years, which is why the team replaced the component rather than patching it again.
Discussion