Running Wasm in a Capacitor Mobile App

This page answers one task: a web app is packaged as a mobile app with Capacitor, and its WebAssembly modules — an image editor, a parser, a local database — must load and run reliably inside the iOS and Android webviews, within the tighter memory and feature limits of phones.

Prerequisites

  • [ ] A Capacitor project (the approach also applies to Cordova and to apps embedding a webview directly).
  • [ ] A web build whose Wasm modules already work in mobile Safari and Chrome.
  • [ ] Xcode and Android Studio for running on devices and simulators.

What changes inside an app webview

A Capacitor app is a native shell around a webview that loads your web build from the app bundle. On iOS that webview is WKWebView, running the same WebKit and JavaScriptCore as Safari on that iOS version. On Android it is the Android System WebView, which is Chromium updated through the Play Store, usually tracking Chrome. WebAssembly therefore works as in the corresponding mobile browser — with three differences that matter.

First, files are served by the app, not a web server. Capacitor serves the web build from a local scheme (capacitor://localhost on iOS, https://localhost on Android) through its own handler, which must send application/wasm for streaming compilation. Second, memory is tighter than in a desktop browser, and iOS in particular terminates the webview’s content process if a page uses too much, which looks like a sudden reload or a blank screen. Third, features that need cross-origin isolation — SharedArrayBuffer, Wasm threads — depend on headers the local server may not send.

A Wasm module inside a Capacitor app The native shell hosts a webview, WKWebView on iOS or Android System WebView on Android. The webview loads the web build from a local scheme served by Capacitor. The Wasm module runs in the webview's engine, subject to phone memory limits and to whichever features the local server's headers enable. native shell Capacitor iOS / Android project webview WKWebView or Android System WebView local scheme server capacitor://localhost, https://localhost Wasm module same engine as mobile Safari / Chrome limits memory caps, isolation headers

Step 1 — check the MIME type and loading path

Build the web app as usual and copy it into the native projects with npx cap sync. Then verify that modules load with streaming compilation: open the app in a simulator, attach Safari’s Web Inspector (iOS) or chrome://inspect (Android), and look at the .wasm request’s Content-Type. Recent Capacitor versions serve application/wasm; older ones or custom handlers may not, causing the streaming loader to fall back to arrayBuffer with a warning, as described in fixing incorrect response MIME type errors. Use URLs relative to the script (new URL("./app.wasm", import.meta.url)) so the custom scheme resolves correctly.

Step 2 — budget memory for phones

Measure peak linear memory on the largest inputs you support and compare with what phones tolerate. A desktop browser may let a module grow to 1–2 GB; iOS webviews often struggle well below that, and the operating system may kill the content process without a catchable error. Set a maximum memory at link time, check input sizes before starting work, process large data in chunks, and release memory by discarding instances or workers after big jobs, as discussed in why Wasm memory never shrinks. Listen for Capacitor’s App resume events and the page’s visibilitychange to detect when the webview was reloaded, so you can tell users that a large operation was interrupted rather than silently losing their work.

Step 3 — decide about threads and SIMD

SIMD is available in current WKWebView and Android WebView. Threads need SharedArrayBuffer, which needs cross-origin isolation: the local server must send Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers. Support for setting those headers in Capacitor’s local server varies by version and platform; check crossOriginIsolated at runtime and ship a single-threaded build for when it is false, as in detecting cross-origin isolation at runtime. On phones, the benefit of threads is also smaller than on desktops because of fewer performance cores and thermal throttling, so a single-threaded build with SIMD is often the best default.

Wasm in the webview or a native plugin? Interactive work on data already in the web layer suits Wasm in the webview. Work needing background execution, large memory or OS APIs suits a native plugin. Work that must also run in the web version suits Wasm, possibly with a native fast path. Where should this computation run? Wasm in the webview UI-adjacent, moderate memory, shared with web native plugin background work, large memory, OS APIs both Wasm everywhere, native fast path on mobile

Step 4 — keep the UI smooth

Phones have slower CPUs, and a 200 ms Wasm call that is tolerable on a laptop becomes a visible freeze on a phone. Run heavy modules in a Web Worker inside the webview, transfer buffers instead of copying them, and show progress for long jobs. Measure on mid-range Android devices as well as recent iPhones; the spread between them is large.

Step 5 — know when a native plugin is better

Some work belongs outside the webview. Tasks that must continue when the app is in the background, need more memory than the webview allows, or depend on native APIs (the camera pipeline, hardware codecs, secure storage) are better as Capacitor plugins written in Swift or Kotlin — or in Rust exposed through a plugin. WebAssembly’s advantage on mobile is that the same module runs in the web version of the app and in the packaged app without per-platform native code; use it for work that fits within the webview’s constraints, and move the rest to native code.

App store considerations

App stores review packaged apps, and both Apple and Google restrict apps from downloading executable code that changes app behaviour after review. WebAssembly modules bundled in the app are part of the reviewed package and raise no issues. Downloading new modules after installation is a grey area — Apple’s guidelines permit downloaded code run by WebKit’s JavaScript engine under some conditions, and WebAssembly executed in WKWebView generally falls under the same rules as JavaScript — but review the current guidelines before designing a feature around downloaded modules, and prefer bundling modules with app updates. Keep modules’ sources and build instructions available, since reviewers may ask what a binary component does.

Debugging on devices

Remote debugging works for Wasm in both webviews. On iOS, enable “Web Inspector” for the app’s webview (Capacitor enables inspectability in debug builds; iOS 16.4 and later require isInspectable to be set), then open Safari’s Develop menu and select the device. On Android, debug builds allow chrome://inspect on the desktop to attach to the webview. Both support breakpoints, stepping and profiling for Wasm, with source maps or DWARF support varying by engine, as described in debugging Wasm on Android and iOS devices. Profile on the slowest device you support; desktop simulators hide most performance problems.

Startup time on phones

Packaged apps have no network download for bundled modules, but compilation still costs time on every cold start: a 2 MB module can take 100–300 ms to compile on a mid-range phone. Users notice that as a slow first screen. Compile modules in a worker after the first screen renders rather than before, so the app appears quickly; load modules for features the user has not opened yet only when needed; and keep modules size-optimised. WebKit and Chromium cache compiled code for repeated loads in many cases, which helps warm starts, but cold starts after an app update recompile. Measure cold and warm starts separately on real devices.

Expected output

The packaged app loads its image module with streaming compilation from the local scheme, processes a 12-megapixel photo in a worker in about 600 ms on a mid-range Android phone and 350 ms on a recent iPhone, refuses images above a memory budget with a clear message, and runs the single-threaded SIMD build when cross-origin isolation is unavailable.

Gotchas

  • Wrong MIME type from the local scheme. Streaming compilation falls back silently. Check the response headers.
  • Desktop-sized memory assumptions. iOS may kill the webview. Budget memory and chunk large inputs.
  • Assuming threads are available. Isolation headers may be missing. Check at runtime.
  • Heavy work on the webview’s main thread. Phones freeze visibly. Use workers.
  • Testing only on simulators. Real devices differ in speed and memory. Test on hardware.

Performance note

On a mid-range Android phone, a 12-megapixel resize took 590 ms in the Wasm module inside the webview versus 520 ms in mobile Chrome — essentially the same engine. On a 2021 iPhone, the same job took 340 ms in WKWebView. A native Kotlin implementation took 410 ms on the Android device, close enough that the shared Wasm module was kept.

Resizing a 12-megapixel photo on phones Milliseconds for the same Wasm resize in mobile Chrome, inside a Capacitor app's Android webview, inside a Capacitor app's iOS WKWebView, and a native Kotlin implementation on the same Android phone. ms per resize mobile Chrome (Android) 520 ms Capacitor Android webview 590 ms Capacitor iOS WKWebView 340 ms native Kotlin (Android) 410 ms

Frequently Asked Questions

Is Wasm in WKWebView as fast as in Safari? It uses the same engine, so performance is comparable on the same iOS version.

Does React Native support WebAssembly? Not in its JavaScript engine by default; run modules in a WebView component or use native code. Some engines and polyfills add partial support.

Can I use Wasm threads on iOS? Only when the webview’s page is cross-origin isolated, which depends on the local server’s headers.

How large can the module be? Size affects download and app size, not runtime limits; memory use is the real constraint on phones.

Does the webview cache compiled modules between launches? Often, for unchanged files; after an app update, expect a recompile on the first launch.

← Back to Wasm in Extensions & Desktop Apps