Testing a Wasm App on a Phone During Development

This page answers one task: run your in-development WebAssembly app on a real phone, connected to the dev server on your laptop, with the same capabilities it will have in production and a debugger attached.

Prerequisites

  • [ ] A phone and a laptop on the same network — or a USB cable.
  • [ ] A dev server you can bind to all interfaces (--host in Vite, 0.0.0.0 elsewhere).
  • [ ] For Android: Chrome on the phone and on the laptop, USB debugging enabled in developer options.
  • [ ] For iOS: Safari on the phone, and Safari on a Mac with the Develop menu enabled.

Why the phone is not optional

WebAssembly’s performance story is told on desktops and lived on phones. A module that compiles in 40 ms on a laptop can take 400 ms on a mid-range Android phone, because baseline compilation is CPU-bound and the phone’s cores are slower and fewer. Memory limits are lower: a tab that happily grows to 2 GB on a desktop may be killed far earlier on a phone with 4 GB of RAM shared across everything. Thermal throttling makes the second minute of a compute-heavy workload slower than the first. And mobile browsers differ in which proposals they ship.

None of that shows up in desktop DevTools’ device emulation, which changes the viewport and throttles the network but runs your module on your laptop’s CPU, in your laptop’s engine. The only way to see real mobile behaviour is to run on a real device — ideally a mid-range one, not the newest flagship.

Desktop emulation versus a real phone for a Wasm app Device emulation in desktop DevTools changes viewport, user agent and network speed, but compilation and execution still use the laptop's CPU and engine. A real phone exposes the compile time, memory pressure, thermal throttling and engine differences that decide mobile performance. desktop device emulation viewport, touch and user agent throttled network laptop CPU compiles the module desktop memory limits layout checks only real mid-range phone real compile and execution times real memory pressure and tab kills thermal throttling over time the mobile engine's feature set the numbers users see

Step 1 — make the dev server reachable

By default most dev servers listen only on localhost, which the phone cannot reach. Bind to all interfaces and find your laptop’s address:

npx vite --host 0.0.0.0 --port 5173
# macOS:   ipconfig getifaddr en0
# Linux:   hostname -I | awk '{print $1}'
# Windows: (Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias Wi-Fi).IPAddress

Open http://192.168.1.20:5173 on the phone. If it times out, the laptop’s firewall is blocking the port; allow the dev server through for private networks only.

This plain-HTTP connection works for basic Wasm, but it is not a secure context — the page is reached by IP address, not localhost. SharedArrayBuffer, the Origin Private File System, WebGPU and service workers are all unavailable. If your app uses any of them, move on to step 2 or step 3.

Step 2 — keep the secure context with USB port forwarding (Android)

Chrome can forward a port on the phone to a port on the laptop over USB. The phone then loads http://localhost:5173, which is a secure context, and the traffic goes over the cable:

  1. Connect the phone by USB and accept the debugging prompt on the device.
  2. On the laptop, open chrome://inspect/#devices.
  3. Click Port forwarding, add 5173 → localhost:5173, and tick Enable port forwarding.
  4. On the phone, open http://localhost:5173.

Command-line users can do the same with adb reverse tcp:5173 tcp:5173. Because the page is on localhost, the cross-origin isolation headers from your dev server are enough to enable SharedArrayBuffer, with no certificates involved.

Step 3 — or use HTTPS with a trusted local certificate (iOS and Android)

iOS has no equivalent of reverse port forwarding for Safari, so for secure-context APIs on an iPhone you need HTTPS with a certificate the phone trusts. Create one with a local CA and install the CA on the device, as described in serving Wasm over HTTPS on localhost. Then open https://192.168.1.20:5173.

Ways to reach the dev server from a phone Four ways to load a local dev server on a phone and whether each gives a secure context, works on iOS, and needs a cable or certificates. approach secure context iOS setup plain http via LAN IP no yes none USB port forwarding yes (localhost) Android only cable + chrome://inspect HTTPS with local CA yes yes CA installed on device public tunnel yes yes traffic leaves your network

Step 4 — attach a debugger

With the page open on the phone, the desktop browser can inspect it as if it were a local tab.

For Android, go to chrome://inspect/#devices on the laptop and click inspect under the page. You get the full DevTools: console, network, performance recording, and the Sources panel with your Wasm module, including source-level stepping if the build has DWARF and the extension from using the C/C++ DevTools Support extension is installed.

For iOS, enable Web Inspector under Settings → Safari → Advanced on the phone, connect it to a Mac, and choose the device from Safari’s Develop menu. Safari’s inspector shows Wasm frames and supports breakpoints in the module, though its source-level support is more limited than Chrome’s.

Step 5 — measure what matters on the device

The point of testing on the phone is the numbers, so record them in a way you can compare across builds. A few lines of instrumentation around the module load capture the phases separately:

const t0 = performance.now();
const response = await fetch(new URL("./pkg/app_bg.wasm", import.meta.url));
const t1 = performance.now();
const module = await WebAssembly.compileStreaming(response);
const t2 = performance.now();
const instance = await WebAssembly.instantiate(module, imports);
const t3 = performance.now();
console.table({
  "fetch headers (ms)": Math.round(t1 - t0),
  "download + compile (ms)": Math.round(t2 - t1),
  "instantiate (ms)": Math.round(t3 - t2),
  cores: navigator.hardwareConcurrency,
  "device memory (GB)": navigator.deviceMemory ?? "n/a",
});

Run it a few times with the cache disabled and then with it enabled. The cached runs show whether the engine’s compiled-code cache is working on the device, which on phones is often the single largest startup win — see reducing Wasm cold-start latency.

Make device testing a routine, not an event

The most useful habit is cheap and boring: keep a mid-range phone on the desk, connected, with port forwarding configured in chrome://inspect, and open the dev URL on it whenever you change something that affects loading or heavy computation. A test that takes thirty seconds when the device is ready takes twenty minutes when it is in a drawer with a flat battery and an expired debugging authorisation, and so it does not happen.

Record the numbers from step 5 alongside the change that produced them. A small spreadsheet or a text file in the repository with date, commit, module size and the three timings is enough to spot a trend — the module that crept from 300 ms to 600 ms of compile time over a quarter, one small dependency at a time. Desktop numbers will not show that trend clearly, because the laptop absorbs a doubling without anyone noticing.

Expected output

On a mid-range Android phone over Wi-Fi, for a 1.8 MB module:

┌─────────────────────────┬──────┐
│ fetch headers (ms)      │ 38   │
│ download + compile (ms) │ 412  │
│ instantiate (ms)        │ 9    │
│ cores                   │ 8    │
│ device memory (GB)      │ 4    │
└─────────────────────────┴──────┘

The same module on the development laptop reported 61 ms for download plus compile — a ratio of almost seven, which is typical.

Gotchas

  • The page loads but threads do not start. The page was reached by IP over HTTP, so it is not a secure context. Use port forwarding or HTTPS.
  • chrome://inspect does not list the device. USB debugging is off, the cable is charge-only, or the authorisation prompt on the phone was dismissed. Reconnect and accept it.
  • The tab reloads on its own during a heavy task. The OS killed it for memory. Check peak memory with performance.measureUserAgentSpecificMemory() and reduce the module’s maximum memory.
  • Numbers vary wildly between runs. Thermal throttling and background apps. Let the phone cool, close other apps, and take the median of several runs.

Performance note

Across three devices, compile time for the same 1.8 MB module ranged from 61 ms on the laptop to 412 ms on a mid-range phone and 940 ms on a four-year-old budget phone. Execution time for the main workload scaled similarly. Testing only on a flagship phone would have hidden most of that spread.

Download plus compile for one 1.8 MB module across devices The same module and network, measured on a development laptop, a current flagship phone, a mid-range phone and a four-year-old budget phone. ms, download + streaming compile, Wi-Fi development laptop 61 ms current flagship phone 168 ms mid-range phone 412 ms four-year-old budget phone 940 ms

Frequently Asked Questions

Can I use an Android emulator instead? An emulator runs the mobile browser on your laptop’s CPU through virtualisation, so its compile and execution times are not representative. It is useful for feature checks, not for performance.

Do I need a Mac to debug Safari on an iPhone? For Safari’s Web Inspector, yes. Some cross-platform tools offer limited inspection from other systems, but the Mac route is the reliable one.

Which phone should I test on? A mid-range Android device two or three years old represents a large share of real traffic. Add an iPhone for Safari-specific behaviour, since JavaScriptCore’s compilation tiers and memory limits differ from Chrome’s in ways that occasionally matter for large modules.

Does the module need a smaller build for phones? Often worth considering. A size-optimized build compiles faster, and lazy loading features you do not need at startup helps more than any flag; see splitting a Wasm module for lazy loading.

← Back to Local Development Server Configurations