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.
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.
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
_startexits. 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.
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.
Related
- Comparing payload size across languages — Swift among other languages.
- Compiling Zig to WebAssembly — another small-binary option.
- Running Wasm modules with the wasmtime CLI — testing WASI builds.
- Building a Wasm module without libc — the bare-metal approach Embedded Swift resembles.
← Back to Other Languages in the Browser