Safari 27 top-level await: remove the workaround without freezing startup

Safari 27 top-level await is now spec-compliant. Test module graphs, startup waterfalls, errors and older-browser fallbacks before removing workarounds.

Safari 27 top-level await tested in a web compatibility lab with a visible module loading trace
Safari 27 fixes a module-loader correctness gap; the application still owns the shape and duration of its dependency chain.

Here is the challenge: find the earliest module in your production graph that waits for configuration, storage, authentication, or a network response. Now name every user-visible module that cannot evaluate until that promise settles.

Safari 27 top-level await is spec-compliant after WebKit rewrote its module loader. That closes the “accessed before initialization” class of Safari-specific failures, but it does not make the waiting itself free.

What Safari 27 top-level await fixes

WebKit says its Safari 27 module-loader rewrite brings full specification compliance for top-level await. Previous implementations could produce unexpected initialization-order errors in valid module graphs.

The team rebuilt host-side loading around the current ECMAScript module model, tested generated graphs, and brought previously failing test262 and Web Platform Tests into compliance. WebKit reports no regressions in that validation and says the new loader was used internally before release.

The semantic rule remains important: when one module pauses at a top-level await, modules that depend on it also pause. Sibling branches that do not depend on the awaited module can continue evaluating.

Predict the graph before running it

Mark the awaited module, its importers, independent siblings, and the first user-visible render. Your prediction is the baseline for the trace: if an unrelated branch waits, the graph or bundler output deserves inspection.

The workaround-removal test

Inventory code added specifically for Safari module ordering. Common shapes include a dynamic import wrapper, a promise exported from an initialization module, duplicated bootstrapping logic, a bundler target override, or a user-agent branch.

Use blame and the original issue to confirm intent before deletion. A strange wrapper may also support an older browser, a test runtime, server-side rendering, or a failure fallback unrelated to WebKit.

Build two artifacts from the same revision: the current path and a candidate with only the Safari workaround removed. Keep dependency versions, minification, server headers, cache state, and feature flags identical.

Safari 27 top-level await migration reviewed through a module dependency sequence on two computers
Original Neyrotex editorial image. A migration test should expose which dependent modules pause, which siblings continue, and how failure reaches the user.

Run a cold navigation with cache disabled and capture the network waterfall, module evaluation timeline, console, first content, and first usable interaction. Repeat with warm cache so a local timing coincidence does not hide the dependency relationship.

Test a delayed promise and a rejected promise. A production graph needs an intentional loading state, timeout, error path, and recovery action rather than a blank shell when initialization does not finish.

Do not put optional work on the critical path

Top-level await fits initialization that must complete before the module’s exports have meaning. Loading a required cryptographic key or opening a local database can qualify when consumers cannot operate safely without the result.

Analytics, recommendations, optional personalization, and below-the-fold data rarely deserve to block the root module. Start them after the first usable frame or isolate them in an independent graph branch.

The MDN await reference notes that top-level await is available in modules. It also warns through the semantics themselves: the importer waits, so the placement of the await determines how far the pause propagates.

Measure the bundler output you actually ship

Bundlers can split, wrap, inline, or reorder modules according to target and optimization settings. Inspect the generated chunks and source maps instead of assuming the source-level graph reaches the browser unchanged.

Record the Safari version, operating system, build hash, bundler version, target list, and server response headers with the trace. Without that identity, a green local run is hard to compare with a customer report.

Keep a small integration page that exercises a dependent branch, an independent sibling, a delayed resolution, and a rejection. Run it in continuous compatibility testing so a later bundler or browser change cannot silently restore the failure.

Add a navigation test as well as a direct-load test. A single-page application can reach the same module after the shell has rendered, while a deep link may need the awaited branch before it can show anything; the user experience and failure recovery are different even when the module graph is identical.

Service workers deserve a separate run with a first install, an activated worker, and a stale cached chunk. A corrected browser loader cannot reconcile HTML and JavaScript from different deployments, so versioned assets and an update recovery path remain part of the migration evidence.

Set an honest compatibility boundary

Safari 27 support does not remove the needs of Safari 26 or an embedded WebView your product still serves. Use traffic and product policy to choose the boundary, then serve a transpiled or alternate path where necessary.

Feature detection is preferable to parsing a browser name, but syntax and module-evaluation behavior can be difficult to probe without executing a controlled graph. A build target or small compatibility bundle may be clearer than runtime branching.

The Neyrotex modern Web API guide covers progressive adoption across engines. The Chrome 153 checklist addresses the faster release cadence on the other major engine.

Follow the Neyrotex Web section for browser changes, but keep this migration grounded in one question: can every supported client reach the first usable frame when the awaited dependency is slow or broken?

The removal rule

Delete the workaround when the candidate matches the current build’s correct output, improves or preserves startup, exposes rejection clearly, and retains a supported path for older clients. Keep it when any one of those claims lacks a reproducible trace.