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

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.