この記事の要点
- iPad 向け手書きノートアプリ Goodnotes(2022年に Apple の iPad App of the Year を受賞)が、iOS アプリと同じ Swift コードを WebAssembly 経由で Web ブラウザ上でも動かしている事例です。2年間の開発と2年以上の本番運用を経て、Swift on WebAssembly が複雑で性能要求の厳しい Web アプリにも実用的だと示しています。
- 鍵となったのは、Swift コードを WebAssembly へコンパイルするコミュニティ主導のプロジェクト SwiftWasm です。10年以上かけて磨き上げた手書きインクのレンダリング、ドキュメント同期、CRDT による競合解決などのコードを書き直さずに、iOS と Web でまったく同じ挙動を保証できました。
- 60FPS を超えるリアルタイム描画を実現するために、WASI Threads と Web Worker による真の並列処理を導入し、その制御に Swift Concurrency の Custom Actor Executors(SE-0392)を活用しています。
- これらの WebAssembly 関連の変更はすべて upstream に取り込まれ、WebAssembly は Swift 6.2 から公式にサポートされるプラットフォームになりました。
なぜ Web で Swift を選んだのか
2021年に Goodnotes を Web へ展開しようと決めたとき、チームは10年以上かけて蓄積した数百万行の Swift コードを抱えていました。デジタルインクの描画、ドキュメント同期、CRDT(Conflict-Free Replicated Data Types)による競合解決、コンテンツ検索やインデックス化といった、無数の改良と最適化が詰まったコードです。
リアルタイムのインク描画では60FPS 超を維持する必要があり、性能が決定的に重要でした。JavaScript での書き直し、Flutter、Kotlin Multiplatform のいずれを選んでも、描画エンジンをゼロから書き直すことになり、Web 版のリリースが何年も遅れるうえ、プラットフォーム間で挙動の差異が生まれることは避けられませんでした。
解決策となったのが SwiftWasm です。性能要求の厳しい手書きコンポーネントでプロトタイプを作って検証したところ、結果は十分に有望で、この道に踏み切りました。最も大きな利点はコードの再利用そのものではなく、挙動の一貫性が保証されることでした。ユーザーが iPad で描いたストロークを後から Web で開いても、まったく同じ曲線・筆圧・インクの流れが再現されます。同じアルゴリズムを2回丁寧に実装し直したからではなく、文字どおり同じ Swift コードが両プラットフォームで動いているからです。
技術アーキテクチャ
アーキテクチャは、プラットフォーム固有の UI コンポーネントと、共有されるビジネスロジックを明確に分離する設計になっています。
共有されるコアと、コード共有の規模
中核を担うのは次の3つです。
- コンテンツ描画エンジン。 ノートの内容とインクストロークをリアルタイムに描画します。低レベルのグラフィックス API(iOS では Metal、Web では WebGL)上に独自の描画エンジンを構築し、描画ロジックはほぼ全面的に共有、プラットフォーム抽象化レイヤーだけを個別に実装しています。
- ビジネスロジック層。 ドキュメントのモデリング、手書き認識、インデックス化はすべて共有の Swift パッケージで実装されています。
- ビューモデル。 ツール操作やジェスチャを扱う中心的なビューモデルもプラットフォーム間で共有しています。
コード共有の規模は、Web 版の Swift コードベース全体が220万行、うち共有コードが147万行(Web アプリの66%、iOS アプリの34%)に達します。行数は最良の指標ではないものの、共有しているビジネスロジックと描画エンジンの大きさを示しています。
最終的な WebAssembly バイナリは約50MB で、Brotli 圧縮により12MB まで小さくなります。読み込みの高速化とキャッシュには Service Worker を利用しています。
JavaScript 相互運用とプラットフォーム差異
Swift と JavaScript のシームレスな相互運用には JavaScriptKit を使い、コアロジックを Swift に保ったまま既存の Web エコシステムと統合しています。
iOS と WebAssembly でコードを共有する際には、いくつかの注意点がありました。
- 並行性モデル。 libdispatch の API は WebAssembly では利用できません。libdispatch の直接利用から、
async/awaitとアクターによる Swift Concurrency へ移行し、クロスプラットフォーム互換性を高めました。 - アーキテクチャの差異。 wasm32 では Swift の
Intが32ビット幅になります。Intが常に64ビットだと仮定していたコードは、明示的にInt64を使うよう修正する必要がありました。 - 依存性注入。 ネットワークアクセスなどの I/O 操作は依存性注入で抽象化し、コアロジックを共有しつつプラットフォーム固有の実装を差し込めるようにしています。
WASI Threads によるマルチスレッド
最も重要な技術的成果のひとつが、WASI(WebAssembly System Interface)Threads と Web Worker、SharedArrayBuffer による真の並列処理の実現です。これにより、手書き認識をバックグラウンドの Web Worker で実行し、ドキュメントのインデックス化でメインスレッドをブロックせず、複雑な処理中も60FPS 超の滑らかな描画を維持できます。
ここで決定的な役割を果たしたのが Swift Concurrency の Custom Actor Executors(SE-0392)です。JavaScript オブジェクトは生成元のスレッドに isolate されるため、Swift のアクターをどのスレッドで実行するかを精密に制御する必要がありました。JavaScriptKit は専用 Web Worker 向けの SerialExecutor を生成する API を提供しており、特定のアクターを特定の Web Worker に固定できます。これにより、手書き認識のような計算負荷の高いタスクはバックグラウンドで動かしつつ、UI 操作はメインスレッドに留め、バックグラウンドスレッドからも JavaScript オブジェクトにアクセスできるようになっています。
このマルチスレッド化により、Interaction to Next Paint(INP)が2倍以上改善し、複雑な操作中の UI 応答性が大きく向上しました。
一方で、SharedArrayBuffer を使うにはCross-Origin Isolation というモダンブラウザのセキュリティ要件を満たす必要があり、アプリにいくらかの複雑さを加えます。この要件を満たせないアプリケーションでは、シングルスレッドの協調的な並行実行も依然として有効な選択肢だとされています。
開発体験とツール
ツールのエコシステムは成熟しており、堅実な開発体験が得られたと述べられています。
- IDE サポート。 Xcode または VS Code(SourceKit-LSP)で、自動補完・エラーチェック・リファクタリングといった言語サーバー機能をフルに使えます。Xcode は現時点で WebAssembly プラットフォームをサポートしていないため WebAssembly 固有 API の補完は限定的ですが、SourceKit-LSP は Swift SDK に対応しており、
.sourcekit-lsp/config.jsonを適切に設定すれば WebAssembly ターゲットでも補完が効きます。 - デバッグ。 Chrome DevTools で Swift コードを直接デバッグできます。ブレークポイントを置き、変数を確認し、JavaScript と同じ感覚で Swift コードをステップ実行できます。Goodnotes は Swift 固有の変数リフレクションとソースレベルデバッグを可能にする Chrome DevTools 拡張ライブラリを開発しています。
- 性能プロファイリング。 Chrome の Performance タブは個々の Swift 関数単位で時間の使われ方を示し、Memory タブからメモリ使用パターンを把握できます。さらに詳細なヒープ解析のために、独自のヒーププロファイラ
wasm-memprofを開発し、標準ツールでは見えにくいメモリ割り当てパターンを可視化しています。
得られた知見と今後
WebAssembly 関連の変更はすべて upstream に取り込まれ、WebAssembly は Swift 6.2 から公式にサポートされるプラットフォームになりました。これにより、他のチームも Goodnotes の成功を支えたのと同じツールと言語機能の恩恵を受けられます。
Swift on WebAssembly は複雑なアプリケーションでも本番運用に耐えると結論づけたうえで、同じ道を検討するチームへの推奨事項が挙げられています。
- 性能要求の厳しいコンポーネントから始めて、アプローチを検証する。
- プラットフォーム抽象化レイヤーへの投資を早めに行う。
- クロスプラットフォーム互換性のために Swift Concurrency を活用する。
- マルチスレッドが必要なら、
SharedArrayBufferのセキュリティ要件を計画に織り込む。 - 既存プロジェクトでは全面的な書き直しよりも段階的な採用を検討する。
iOS を動かす言語が、いまや高速・安全・保守しやすい Web 体験も生み出せる——Swift のクロスプラットフォーム化が進む様子を示す事例です。