The short version: yes, Swift compiles to WebAssembly, officially — swift.org publishes WASM Swift SDKs you install via swiftly and build with swift build --swift-sdk …. Xcode's bundled toolchain cannot do it. A stdlib-only module lands around 5 MB, import Foundation takes it to roughly 57 MB, and Embedded Swift gets the same logic into kilobytes — with restrictions that can fail your link on something as innocent as string interpolation.
We compile other people's Swift to WASM for a living — it's how Patch ships OTA updates to native iOS apps — which means we've hit most of the walls in this territory at least twice. Here's the map.
How to compile Swift to WebAssembly: toolchain setup
The first wall: Xcode's Swift cannot target WebAssembly. Apple builds its LLVM without the WebAssembly backend, so swiftc -target wasm32-unknown-wasip1 from an Xcode toolchain just errors out. You need three things (the official getting-started guide covers all of them):
The swift.org toolchain, via swiftly:
swiftly install 6.3.2The WASM Swift SDK matching it:
swift sdk install <swift.org artifactbundle URL> --checksum <sha256>A build against it:
swift build --swift-sdk swift-6.3.2-RELEASE_wasm
(If you used the community SwiftWasm toolchain in years past: as of Swift 6 the official swift.org SDKs replace it for most uses.)
Now you have two Swift toolchains on one machine, and which one swift resolves to is decided by your PATH. This bit us with a genuinely evil failure mode: our build pipeline silently selected the Xcode toolchain (because /usr/bin came first), and instead of an error we got "0 modules emitted" — everything "worked," nothing was produced. The fix is boring and absolute:
# compiling to WASM — swiftly first:
export PATH="$HOME/.swiftly/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"; hash -r
# building/testing for the host — Xcode toolchain first:
export PATH="/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin"; hash -rThe hash -r matters; your shell caches the resolved path of swift. (PATH-order collisions bit us twice — we also had to rename our CLI over one.)
Target-wise you're compiling for WASI Preview 1 (wasm32-unknown-wasip1). One concept worth knowing early: a command module has a main, runs, and exits; a reactor is a passive library the host calls into. If you're embedding Swift-in-WASM inside another app (our case: an OTA module the SDK invokes), you want a reactor — built by passing -mexec-model=reactor to the linker (through SwiftPM: -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor).
And the good news, before the walls: it's really Swift. Generics, protocols, closures, value semantics, the whole standard library, Foundation via the corelibs port — the real compiler, compiling your real code. Even async/await compiles fine. Making it actually run inside a host with no event loop is a separate adventure.
Swift WebAssembly binary size: 5 MB hello-world, 57 MB with Foundation
WASM binaries statically link everything — the Swift runtime, the stdlib, whatever corelibs you import. Our measurements with Swift 6.3, release builds:
| What you compile | Module size |
|---|---|
| Pure-logic module, stdlib only | ~5.3 MB |
Same module + import Foundation | ~57 MB |
| Same logic, Embedded Swift | ~11 KB |
The 57 MB isn't a bug: Foundation drags in ICU, and ICU means the Unicode Consortium's collation and normalization tables, statically, in every module. wasm-opt and stripping help at the margins; the order of magnitude stands.
The lever that changes the game is Embedded Swift (-enable-experimental-feature Embedded, whole-module, with the embedded WASM SDK variant): a subset of Swift designed to compile without the runtime's dynamic machinery. Same pure logic, ~11 KB — about 480× smaller than the stdlib build.
One real measurement from our pipeline, with the honest caveat that it's our best case (a pure-logic, embedded-clean closure), like-for-like at each step:
full-SDK module: 60.1 MB raw → Embedded Swift +
wasm-opt: 23.9 KB raw (~2,500×) → brotli, on the wire: 8.8 KB
That last number is the difference between an OTA update costing megabytes or kilobytes on a user's cell connection. Code that genuinely needs Foundation doesn't get this: it ships at the tens-of-MB tier, which is why our Foundation-dependent modes are opt-in rather than default.
Embedded Swift limitations: what you give up
Embedded Swift is a subset. Most notably you lose:
All of Foundation — no
JSONDecoder, noDate/DateFormatter/Locale, noURL, noUUID, noDecimal, noNSRegularExpression.Codable— it's standard library, not Foundation, but Embedded excludes it independently: it depends on runtime reflection metadata the subset removes.Reflection (
Mirror), unrestricted existentials, metatype dynamic casts; generics must be fully specializable at compile time.
And then there's the trap that looks like a toolchain bug and isn't.
Fixing wasm-ld: error: undefined symbol: _swift_stdlib_getNormData
Build an Embedded Swift WASM module with perfectly ordinary string code and you can hit this at link time:
wasm-ld: error: undefined symbol: _swift_stdlib_getNormData
wasm-ld: error: undefined symbol: _swift_stdlib_getComposition
wasm-ld: error: undefined symbol: _swift_stdlib_nfd_decompositionsNothing is wrong with your SDK. Swift's String is grapheme-cluster-correct, and correctness needs data: Unicode normalization tables the embedded stdlib doesn't link by default. Innocuous-looking operations reach for them:
| You wrote | It needs |
|---|---|
"value: \(x)" (interpolation) | Unicode normalization tables |
if s == "ok" / switch on a String | same |
s.prefix(5), grapheme/Character ops | same, plus extended-pictographic tables |
Double("3.14") | _swift_stdlib_strtod_clocale |
The documented fix is to link the tables back in — libswiftUnicodeDataTables.a from the toolchain's resource directory — at a few hundred KB of size cost. If you're chasing kilobyte-class modules, the cheaper route is to not need the tables at all:
| Instead of | Use |
|---|---|
| interpolation | + concatenation, String(Int) / String(Double) — all link-clean |
String == | compare Array(s.utf8) byte-wise * |
.prefix(n) | truncate UTF-8 bytes, backing off continuation bytes (0x80...0xBF) * |
Double(String) | a ~20-line hand-rolled decimal scan |
* One honest caveat: byte-wise equality is not Unicode canonical equivalence — precomposed and decomposed "é" compare unequal. It's valid when you control both sides of the comparison (ASCII literals, known-normalized data), which in a wire-protocol context you usually do. That's a semantics tradeoff you're choosing, not a free lunch.
Also safe, for the record: String(decoding: bytes, as: UTF8.self) and iterating s.utf8 or s.unicodeScalars. The pattern underneath: anything that interprets Unicode (normalizing, grapheme-breaking, locale-aware parsing) needs the tables; anything that treats strings as UTF-8 bytes doesn't.
In our pipeline we don't ask developers to write this dialect — the engine detects code that isn't embedded-clean and automatically falls back to the full SDK for it. But if you're hand-writing Embedded Swift for WASM, the table above is a few days of head-scratching, prepaid.
Running Swift WASM modules: wasmtime, and WasmKit on iOS
On a dev machine, wasmtime runs WASI modules directly and is the fastest feedback loop. For embedding inside a Swift app there's WasmKit — a WebAssembly runtime written in pure Swift (it also ships as the wasmkit CLI inside swift.org toolchains since Swift 6.2). Two properties make it more than a curiosity: it's an interpreter, no JIT — meaning it runs on iOS, where third-party apps get no runtime code generation — and it speaks WASI, so Foundation-linked modules work (in our builds, a Foundation module imports ~34 wasi_snapshot_preview1 functions; even minimal ones want around 14, all provided by WasmKitWASI).
That interpreter-on-iOS property is what makes "compiled Swift, downloaded and executed on an iPhone, with zero runtime code generation" a sentence that parses. Whether and how you may ship code that way is a separate, policy question — we wrote up Apple's actual rules here; the compliance is yours to own either way.
What this adds up to
If you're evaluating Swift-on-WASM in 2026: it's real, it's officially supported, the language coverage is excellent — and the engineering lives in the size/capability tradeoff. Full-SDK builds give you all of Swift at tens of megabytes; Embedded gives you kilobytes at the cost of Foundation and some string ergonomics. Our answer is to compile each piece of code for the smallest tier it fits and fall back automatically when it doesn't fit — yours might be simpler. Either way, now you know where the walls are.
Patch compiles changed Swift to WebAssembly and ships it OTA to native iOS apps — patchrelease.com, docs. Related: Running Swift async/await inside WebAssembly · WebAssembly host functions in Swift.