Simulating Slow Networks for Wasm Loading

This page answers one task: on a fast development machine and network, the WebAssembly feature loads instantly, but users on phones and mobile networks report waiting, and you want to reproduce their experience locally and measure it, so loading strategies can be compared before they ship.

Prerequisites

  • [ ] A production build (compressed, minified) served locally or from staging.
  • [ ] Chrome DevTools, and optionally Playwright or WebPageTest for scripted runs.
  • [ ] A rough idea of your users’ networks and devices from analytics or field data.

What makes Wasm loading slow for real users

WebAssembly loading has three costs: transferring the bytes, compiling them, and instantiating plus running startup code. A developer machine hides all three — local servers deliver megabytes in milliseconds, and fast CPUs compile them quickly. On a mid-range phone over a congested 4G connection, a 2 MB compressed module can take two seconds to download and several hundred milliseconds to compile. Streaming compilation overlaps the two, so the total is not simply the sum, but only when headers and loading code are right.

Simulation reproduces both constraints. Network throttling limits bandwidth and adds latency; CPU throttling slows script execution and compilation. Neither is perfect — DevTools throttling shapes requests at the browser level rather than the radio, and CPU throttling scales JavaScript work more faithfully than it scales Wasm compilation threads — but both make problems visible that are otherwise invisible during development.

Loading a 2 MB module on a throttled profile On a slow 4G profile with 4x CPU throttling, the request starts, bytes arrive over about two seconds while streaming compilation keeps pace, compilation finishes shortly after the last byte, the module is instantiated, and the first call completes. 0 ms request starts 180 ms first bytes (latency) 2,100 ms last byte 2,350 ms compiled (streaming) 2,420 ms instantiated 2,700 ms first call done

Step 1 — throttle network and CPU in DevTools

In the Network panel, choose a preset (“Slow 4G”, “Fast 4G”) or a custom profile — for example 1.6 Mbps down, 150 ms latency — and in the Performance panel set CPU throttling to 4× or 6×. Disable the cache for cold-load tests. Then record a Performance trace while loading the feature. The trace shows the .wasm request, compile tasks (v8.wasm.compileStreaming or similar), instantiation and the first call, each with its duration on the throttled profile.

Step 2 — compare cold and warm loads

Users who return find the module in the HTTP cache and, often, compiled code in the browser’s code cache. Test three states: cold (cache disabled), warm HTTP cache (cache enabled, second load), and warm code cache (third load, where Chrome reuses compiled code for unchanged responses). The difference between cold and warm loads shows how much caching strategy matters; if warm loads are not much faster, check Cache-Control headers and whether the URL changes between loads, as described in setting cache-control headers for Wasm.

Step 3 — script repeatable runs

Manual throttling is good for exploration; scripts make comparisons reliable. With Playwright and the Chrome DevTools Protocol:

import { chromium } from "playwright";

const browser = await chromium.launch();
const page = await browser.newPage();
const cdp = await page.context().newCDPSession(page);
await cdp.send("Network.enable");
await cdp.send("Network.emulateNetworkConditions", {
  offline: false, latency: 150, downloadThroughput: 1.6e6 / 8, uploadThroughput: 750e3 / 8,
});
await cdp.send("Emulation.setCPUThrottlingRate", { rate: 4 });

await page.goto("http://localhost:4173/editor", { waitUntil: "load" });
const t = await page.evaluate(() => performance.getEntriesByName("wasm-ready")[0]?.startTime);
console.log(`wasm ready at ${t?.toFixed(0)} ms`);

Have the page set a performance.mark("wasm-ready") when the module is usable, so scripts measure the moment users care about. Run each configuration several times and report the median; throttled runs still vary.

Test matrix for Wasm loading Combine network profiles and cache states to see loading under realistic conditions. A fast connection with a cold cache shows compile cost; slow 4G cold shows download cost; warm HTTP cache removes download; warm code cache also removes compilation. profile cache state what it reveals fast connection cold compile + startup cost slow 4G + 4× CPU cold worst realistic first visit slow 4G + 4× CPU warm HTTP cache compile without download slow 4G + 4× CPU warm code cache startup only

Step 4 — try loading strategies under throttling

With a repeatable setup, compare strategies on the same profile: eager versus lazy loading, preloading on hover or idle, compression levels, splitting the module, compiling in a worker. The throttled numbers make trade-offs concrete — for example, that preloading on hover saves 800 ms of perceived wait on slow 4G but nothing on fast connections. Strategies are covered in lazy loading Wasm on first use and preloading Wasm with link rel=preload.

Step 5 — validate against real devices and field data

Simulation approximates; real devices decide. Periodically run the same scripted scenario on a real mid-range Android phone over a real mobile connection (remote debugging or a device lab), and compare with field data from real-user monitoring. If field data is consistently worse than the simulation, adjust the profile — slower bandwidth, higher latency, stronger CPU throttling — until the simulation predicts reality, then use it to evaluate changes before they ship.

Testing in CI with performance budgets

A throttled loading test in CI turns these measurements into a guard. Run the scripted scenario against the production build on every change, record time to wasm-ready on the slow profile, and fail when it exceeds a budget or regresses beyond a tolerance from the main branch’s baseline. Throttled timings in CI vary with machine load, so use medians of several runs and generous tolerances, and run the job on dedicated or consistent runners. Even a coarse guard catches the large regressions — a module that doubled in size, compression that stopped working, a lazy import that became eager — that matter most to users on slow networks.

Lighthouse and WebPageTest

Lighthouse’s mobile profile applies simulated throttling and reports Largest Contentful Paint, Total Blocking Time and Time to Interactive, which capture how a Wasm module on the critical path affects page load. It does not measure when your feature becomes usable unless you add a mark and read it from the trace. WebPageTest runs real browsers on real devices and network shaping at the packet level, closer to reality than DevTools throttling, and its filmstrip view shows what users see while the module loads. Use Lighthouse in CI for page-level budgets and WebPageTest for periodic deep dives.

Simulating flaky and interrupted connections

Slow is not the only network problem users have. Mobile connections drop, stall and recover; captive portals return HTML instead of the requested file; corporate proxies time out long downloads. A WebAssembly feature that downloads megabytes is exposed to all of these. Test them deliberately: use the DevTools “Offline” toggle mid-download, Playwright’s request interception to abort the .wasm request after a delay or return a truncated body, and a local proxy such as toxiproxy to add jitter and packet loss. Check that the loader reports a clear error, offers a retry, and does not leave the feature in a half-initialised state where every later call fails. A service worker that caches the module after the first successful load makes later visits resilient, and the same interception tools can confirm that the cached path works offline.

Communicating loading to users

The point of simulating slow networks is to design for them. Once you can see a two-second wait, decide what the user sees during it: a progress indicator driven by download progress (readable from a streamed fetch with known Content-Length), a usable interface with the Wasm-dependent controls disabled and labelled, or a lighter JavaScript fallback that works until the module arrives. Each choice can be tested on the throttled profile, including how it feels when the download fails halfway.

Expected output

On the slow 4G profile with 4× CPU throttling, the editor’s module is ready at 2.7 s on a cold load, 0.6 s with a warm HTTP cache and 0.3 s with a warm code cache; preloading on hover reduces perceived cold-load wait to 0.9 s; and a CI job fails if the cold-load figure exceeds 3.2 s.

Gotchas

  • Testing the development build. Uncompressed, unminified output distorts timings. Test the production build.
  • Forgetting cache state. Cold and warm loads differ by seconds. Test both explicitly.
  • Single runs. Throttled timings vary. Report medians.
  • Trusting simulation alone. Calibrate against real devices and field data.
  • Measuring page load instead of feature readiness. Mark when the module is usable.

Performance note

Moving the 2 MB module from an eager import to lazy loading with preload-on-hover cut Largest Contentful Paint on the slow 4G profile from 3.4 s to 1.9 s, while the feature itself became ready 0.4 s later than before when users clicked immediately — a trade-off visible only under throttling.

Time to a ready module on the slow 4G profile Seconds until the module is usable on a slow 4G profile with 4x CPU throttling, for a cold cache, a warm HTTP cache and a warm code cache. seconds to wasm-ready cold cache 2.7 s warm HTTP cache 0.6 s warm code cache 0.3 s

Frequently Asked Questions

Does CPU throttling slow Wasm compilation realistically? Approximately. Compilation runs on background threads that throttling affects less; validate on real devices.

Which network profile should I use? Start from your field data’s 75th-percentile connection; “Slow 4G” is a reasonable default for mobile audiences.

Can I throttle in Firefox? Firefox DevTools offers network throttling; CPU throttling is Chromium-only.

Should CI fail on throttled timings? Yes, with medians and tolerances, for large regressions; small changes are noise.

How do I test a download that fails halfway? Abort or truncate the .wasm response with Playwright’s request interception, and check the loader’s error and retry behaviour.

← Back to Local Development Server Configurations