Compiling Swift to WebAssembly

This page answers one task: a team with Swift code — shared logic from an iOS app, a server-side Swift library, or a developer who simply prefers Swift — wants it compiled to WebAssembly and running in the browser or a WASI runtime.

Prerequisites

  • [ ] A Swift 6 toolchain from swift.org (not the Xcode-bundled one), on macOS or Linux.
  • [ ] The Swift SDK for WebAssembly matching that toolchain version.
  • [ ] Node or a static server to run the browser output; optionally wasmtime for WASI.

The state of Swift on WebAssembly

Swift’s WebAssembly support grew out of the community SwiftWasm project and has been moving into the official toolchain: Swift 6 can install a WebAssembly SDK through swift sdk install and cross-compile packages with SwiftPM to wasm32-unknown-wasi. The output is a WASI module containing your code, the Swift runtime and the parts of the standard library and Foundation you use. In the browser, a JavaScript shim provides WASI, and the JavaScriptKit library lets Swift call JavaScript and the DOM.

Two shapes of Swift exist for Wasm. Full Swift supports the whole language — classes, protocols with existentials, reflection, Foundation — at the cost of a large binary: several megabytes for a simple program because the runtime, Unicode data and Foundation come along. Embedded Swift is a restricted subset designed for microcontrollers and small binaries: no runtime reflection or existential-heavy features, but binaries of tens of kilobytes. Choosing between them is the main decision.

Full Swift versus Embedded Swift for Wasm Full Swift supports the complete language and Foundation but produces multi-megabyte binaries. Embedded Swift restricts the language — no reflection or full existentials — and produces binaries in the tens of kilobytes, suited to small libraries. full Swift complete language + Foundation runtime and Unicode tables included several MB per binary apps and existing code Embedded Swift restricted subset no reflection, limited existentials tens of KB per binary small libraries

Step 1 — install the toolchain and SDK

swift --version                                     # e.g. Swift version 6.1 from swift.org
swift sdk install <URL-of-matching-wasm-sdk-artifactbundle> --checksum <sha256>
swift sdk list                                      # shows the installed wasm32-unknown-wasi SDK

The SDK version must match the compiler version exactly; the SwiftWasm project and swift.org publish the matching URLs and checksums for each release. Mismatches produce confusing module-loading errors, so pin both in CI and record the versions in the repository, for example in a .swift-version file next to the SDK URL.

Step 2 — build a package for WASI

swift package init --type executable --name Greeter
swift build --swift-sdk wasm32-unknown-wasi -c release
wasmtime .build/release/Greeter.wasm               # prints "Hello, world!"

The build produces a WASI command module. Running it in wasmtime first confirms the toolchain works before adding browser concerns. Strip and optimise the release output with wasm-opt -Os and, if you do not need it, remove debug information with wasm-strip or llvm-strip.

The same .wasm runs in any WASI host — wasmtime, WasmEdge, Node’s node:wasi — which makes it easy to test Swift logic on the server before wiring up a browser UI.

Step 3 — call JavaScript with JavaScriptKit

For browser apps, add JavaScriptKit as a dependency and use its JSObject API, which mirrors JavaScript’s dynamic object model:

// Package.swift: .package(url: "https://github.com/swiftwasm/JavaScriptKit", from: "0.20.0")
import JavaScriptKit

let document = JSObject.global.document
var heading = document.createElement("h1")
heading.innerText = "Hello from Swift \(1 + 1)"
_ = document.body.appendChild(heading)

let button = document.getElementById("go")
button.onclick = .object(JSClosure { _ in
    JSObject.global.console.log("clicked")
    return .undefined
})

JavaScriptKit’s runtime JavaScript must be loaded alongside the module; its packaging plugin (swift package js) generates a bundle with the Wasm file, the runtime and a WASI shim. Closures passed to JavaScript must be kept alive while JavaScript may call them and released afterwards — the same lifetime discipline as in other languages, described in passing closures between Rust and JavaScript.

Building and running Swift in the browser SwiftPM compiles the package with the WebAssembly SDK into a WASI module. JavaScriptKit's packaging step adds its JavaScript runtime and a WASI shim. The page loads the bundle, the shim provides WASI, and Swift code manipulates the DOM through JavaScriptKit. Swift package Sources + JavaScriptKit swift build --swift-sdk wasm32-unknown-wasi swift package js bundle + runtime + shim browser loads bundle WASI shim instantiates Swift runs DOM via JSObject

Step 4 — export Swift functions for JavaScript callers

To use Swift as a library from an existing JavaScript app, expose C-ABI functions with @_cdecl (or the newer @_expose(wasm) attribute) and call them from JavaScript after instantiation:

@_expose(wasm, "checksum")
@_cdecl("checksum")
public func checksum(_ ptr: UnsafePointer<UInt8>, _ len: Int32) -> UInt32 {
    var h: UInt32 = 2166136261
    for i in 0..<Int(len) { h = (h ^ UInt32(ptr[i])) &* 16777619 }
    return h
}

The module should be built as a reactor (-Xlinker --no-entry or the appropriate SwiftPM settings) so it exposes functions without running main. JavaScript copies input into linear memory and calls the export, exactly as for C modules, using the techniques in passing arrays between JavaScript and Wasm.

Step 5 — use Embedded Swift for small modules

For small libraries, compile with Embedded Swift:

swiftc -target wasm32-unknown-none-wasm -enable-experimental-feature Embedded -wmo -Osize \
  -Xcc -fdeclspec -Xlinker --no-entry -Xlinker --export=checksum checksum.swift -o checksum.wasm

The result has no WASI dependency and is typically a few kilobytes to tens of kilobytes. You give up dynamic features — no Any boxing of arbitrary types in some positions, no runtime reflection, limited Foundation — but numeric and data-processing code ports cleanly. It is the right tool for a Swift checksum, parser or codec embedded in a web page.

Reducing full-Swift binary size

When the full language is needed, several measures bring the binary down. Avoid importing Foundation in modules that do not need it; much of a typical program’s size comes from Foundation and the ICU-derived Unicode data it carries. Prefer the smaller FoundationEssentials module where the SDK offers it. Build with -Osize and whole-module optimisation, then run wasm-opt -Os or -Oz, which removes dead code the Swift compiler kept and shrinks function bodies. Strip debug information and the name section from release builds, and keep a separate unstripped build for symbolicating crashes. Measure each step: for small apps, dropping Foundation often halves the binary, and the remaining runtime cost is largely fixed. Serve with Brotli, which compresses Swift binaries well, and cache the result with a content hash so returning visitors pay the cost only once. Even then, full Swift is best for apps where the download is acceptable — tools, games, internal dashboards — rather than lightweight enhancements to content pages.

Sharing code with Apple platforms

The most common motivation is reuse: an iOS app already contains validation, formatting or document-handling logic in Swift, and the web version should behave identically. Put that logic in a separate Swift package with no UIKit or SwiftUI imports, and depend on it from both the app and the Wasm target. Avoid Apple-only frameworks in the shared package — CoreGraphics, CryptoKit, os_log — or wrap them behind protocols with platform-specific implementations. Foundation is available in the Wasm SDK through swift-corelibs-foundation, but some APIs behave differently or are missing, notably those depending on the operating system: networking, file coordination, locale databases. Write tests in the shared package and run them on macOS and under wasmtime in CI; the Wasm SDK supports swift test through a WASI runner. Differences surface as failing tests on one platform rather than as user reports, which makes sharing code across iOS and the web practical rather than aspirational.

Expected output

The full-Swift greeter runs in wasmtime and, packaged with JavaScriptKit, adds a heading to a page; the Embedded Swift checksum module is under 10 KB and returns the same FNV-1a hash as the native build for the same input.

Gotchas

  • SDK and compiler version mismatch. Builds fail with module errors. Install the SDK that matches the toolchain exactly.
  • Using the Xcode toolchain. It lacks Wasm support. Use a swift.org toolchain.
  • Huge binaries with full Swift. Expect megabytes. Use Embedded Swift for small libraries, and wasm-opt -Os.
  • Building a command instead of a reactor. Exports are not callable after _start exits. Build with no entry point for libraries.
  • Leaking JSClosures. Release closures when JavaScript no longer needs them.
  • Apple-only frameworks in shared code. They do not exist on Wasm. Keep the shared package framework-free.

Performance note

A hello-world in full Swift with Foundation was about 9 MB raw and 2.4 MB with Brotli after wasm-opt -Os; without Foundation, about 4 MB raw. The Embedded Swift checksum module was 1.6 KB. Hashing 10 MB ran in 21 ms in both the full and embedded builds — size, not speed, is the difference.

Binary size by Swift flavour Kilobytes of optimised Wasm for a small program built with full Swift and Foundation, full Swift without Foundation, and Embedded Swift. KB of Wasm (optimised, uncompressed) full Swift + Foundation 9,000 KB full Swift, no Foundation 4,000 KB Embedded Swift 1.6 KB

Frequently Asked Questions

Can SwiftUI run in the browser? Not Apple’s SwiftUI. Community projects such as Tokamak implement SwiftUI-like APIs on top of the DOM.

Is async/await supported? Yes in full Swift; JavaScriptKit provides an executor that integrates Swift concurrency with JavaScript Promises.

Does Swift on Wasm support threads? Experimental support exists with the wasi-threads target; most browser apps use a single thread.

How does Swift compare with Rust for Wasm size? Full Swift is far larger; Embedded Swift is competitive with Rust for small modules.

Can I debug Swift Wasm code in the browser? With DWARF debug information and the C/C++ DevTools extension, Chrome can step through Swift source in development builds.

Can Swift packages from the wider ecosystem be used? Pure-Swift packages without platform-specific dependencies usually build; packages that depend on Darwin or Glibc APIs need conditional code.

← Back to Other Languages in the Browser