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 (
--hostin Vite,0.0.0.0elsewhere). - [ ] 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.
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:
- Connect the phone by USB and accept the debugging prompt on the device.
- On the laptop, open
chrome://inspect/#devices. - Click Port forwarding, add
5173→localhost:5173, and tick Enable port forwarding. - 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.
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://inspectdoes 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.
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.
Related
- Serving Wasm over HTTPS on localhost — the certificate setup iOS testing needs.
- Measuring Wasm compile time in DevTools — reading the same numbers in a trace.
- Measuring inference latency in the browser — a workload where device spread is extreme.
- Measuring Wasm performance with real-user monitoring — collecting the same numbers from users.
← Back to Local Development Server Configurations