Blog › ICP guides
WebAssembly developer on retainer: WASM, Emscripten, WASI, and JS integration on monthly retainer
September 19, 2026 · ~21 min read
A fintech company had a high-performance risk calculation engine written in C++ that processed option pricing models running on their server cluster. They needed the same engine available in their browser-based trading dashboard — running client-side for latency, not round-tripping to a server for each calculation. They hired a WebAssembly developer on retainer to port the C++ engine to WASM using Emscripten. The first compilation succeeded within a day: emcc pricing_engine.cpp -O2 -o pricing.js -sEXPORTED_FUNCTIONS=['_calculate_option_price','_calculate_greeks'] -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap']. The resulting pricing.js loaded correctly in Chrome. The pricing engine produced correct results for all test cases.
Three problems surfaced in the first week of integration. First, the engine called a blocking I/O function to load a volatility surface data file at startup — in the browser context, this blocked the JavaScript event loop, freezing the UI for 400ms on every page load. Second, the client wanted to run the Greeks calculation across a portfolio of 10,000 options in parallel using Web Workers — the engine used global state that was not safe for concurrent access. Third, the compiled binary was 4.2MB, too large for the trading dashboard's 2MB bundle budget for third-party modules. Each problem required a different class of retainer work: ASYNCIFY instrumentation for the blocking I/O issue, SharedArrayBuffer memory design for the threading issue, and Binaryen wasm-opt optimization for the size issue. None of the three produced a meaningful code diff. The ASYNCIFY solution involved adding two flags and specifying a function list. The SharedArrayBuffer solution involved three HTTP response headers and a memory layout change. The wasm-opt solution involved a single Binaryen command with four flags. The invisible work was the 36 hours of diagnosis, instrumentation analysis, header configuration, and size optimization that preceded each two-line diff.
A WebAssembly developer on monthly retainer does this category of work continuously: diagnosing ASYNCIFY instrumentation overhead before it causes bundle size regressions, configuring SharedArrayBuffer COEP/COOP headers before thread synchronization failures surface in production, and auditing Binaryen optimization passes before binary size limits force feature cuts.
WebAssembly binary format, WAT, and the module section model
Understanding the WebAssembly binary format at the section level is the foundation for diagnosing toolchain-produced binaries and writing correct WAT for bare-metal WASM modules. A WASM binary file starts with a 4-byte magic number (\0asm) and a 4-byte version (1\0\0\0) followed by a sequence of sections, each identified by a 1-byte type code and a LEB128-encoded byte length. The standard sections in order are: type (function signatures as vectors of parameter and result value types), import (external functions, tables, memories, and globals), function (indices into the type section for each locally defined function), table (typed function-reference tables for indirect calls), memory (linear memory declarations with initial and optional maximum page counts), global (mutable and immutable globals with initializer expressions), export (exported functions, tables, memories, and globals by name), start (optional single function index called on module instantiation), element (initialization data for table function-reference entries), code (function bodies as local variable declarations plus instruction sequences), data (initialization bytes for linear memory segments), and custom (toolchain-specific sections like name, producers, and DWARF debug info). Knowing which section contains what allows targeted binary analysis with wasm-objdump -h module.wasm to see section sizes and identify, for example, that 900KB of a 1.2MB binary is in the code section versus 40KB in data — indicating dead function bloat from unused library code rather than large static data.
WebAssembly value types are the primitive types available for function parameters, results, locals, and globals: i32 and i64 for integers, f32 and f64 for floats, v128 for 128-bit SIMD vectors (requiring the SIMD proposal), funcref for typed function references (requiring the reference types proposal), and externref for opaque external JS object references. SIMD instructions on v128 — i8x16.add, f32x4.mul, i16x8.relaxed_q15mulr_s from the relaxed SIMD proposal — are generated by Emscripten with -msimd128 and benefit from Binaryen's SIMD optimization passes. Reference types (externref, funcref) enable storing opaque JS references in WASM tables without going through the linear memory, eliminating the unsafe integer handle pattern where JS object references were encoded as integers and stored in a JS-side handle map. The bulk memory operations proposal — memory.copy, memory.fill, memory.init, and data.drop — enables efficient large memory initialization and copy at the instruction level, replacing hand-written memcpy loops in WAT with single instructions that the engine can optimize using platform memcpy intrinsics.
WAT (WebAssembly Text Format) is the human-readable S-expression syntax for WASM binaries. Writing WAT directly is most common for bare-metal WASM modules that have no C/Rust source — custom allocators, hand-tuned numerical kernels, or toolchain-independent host-callable APIs. A minimal WAT module with a memory import, a function import, and an exported add function illustrates the structure: (module (import "env" "memory" (memory 1)) (import "env" "log_i32" (func $log (param i32))) (func $add (export "add") (param $a i32) (param $b i32) (result i32) local.get $a local.get $b i32.add) ). The wasm2wat tool from Binaryen or the wabt toolkit disassembles any WASM binary into equivalent WAT for inspection — essential for auditing Emscripten or wasm-pack output for unexpected imports, dead exports, or oversized function tables that contribute to binary size.
Emscripten toolchain, ASYNCIFY, and browser integration
Emscripten compiles C and C++ to WebAssembly via the LLVM wasm32 backend, generating both a .wasm binary and a JavaScript glue file that handles module initialization, memory management, and JS/WASM type bridging. The core output flags — -o output.js generates glue JS plus a sibling output.wasm; -o output.html adds an HTML test harness; -o output.mjs generates ES Module glue — determine the integration pattern for the consuming JS bundle. Critical configuration flags: -sEXPORTED_FUNCTIONS=['_fn1','_fn2'] lists C function names (prefixed with _) to export into the JS glue's Module object; -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8'] enables runtime helper methods; -sALLOW_MEMORY_GROWTH allows the WASM linear memory to grow beyond its initial size at runtime (required for any workload with variable-size allocations); -sINITIAL_MEMORY=67108864 sets the initial memory in bytes (64MB, aligned to 65536-byte WASM page size); -sMODULARIZE=1 -sEXPORT_NAME=MyModule wraps the glue in a factory function that returns a Promise resolving to the initialized module — required for use with bundlers like webpack and Vite that expect ES Module or factory patterns rather than implicit global side effects. Emscripten system library ports — -sUSE_SDL=2 for SDL2, -sUSE_ZLIB=1 for zlib, -sUSE_LIBPNG=1 for libpng — compile and link prebuilt system libraries into the output without requiring separate library compilation steps.
ASYNCIFY is Emscripten's mechanism for converting synchronous C/C++ code that calls blocking operations into asynchronous WASM that yields to the JS event loop. Without ASYNCIFY, any C function that blocks — calls SDL_Delay, performs synchronous network I/O via Emscripten's Fetch API with EMSCRIPTEN_FETCH_SYNCHRONOUS, or calls emscripten_sleep — freezes the browser tab for the duration of the block. With -sASYNCIFY, Emscripten instruments the WASM binary to save and restore the entire call stack when an async operation is initiated, yielding control to the JS event loop and resuming from the saved stack when the async operation completes. The instrumentation applies to every function reachable from a blocking import by default — typically the full call graph — causing 2x to 4x binary size growth. The two controls for limiting this growth are ASYNCIFY_ONLY — a list of function names to instrument, applied when the blocking calls are isolated to a small subset of the call graph — and ASYNCIFY_LAZY_LOAD_CODE, which moves the ASYNCIFY unwinding code into a separately loaded binary segment that is fetched only when a blocking call is actually triggered at runtime, keeping the initial parse cost low. The ASYNCIFY_IMPORTS list names the JS-side functions that Emscripten should treat as blocking — every JS import not in this list is assumed non-blocking and will not trigger stack unwinding. Auditing ASYNCIFY_IMPORTS completeness is essential: a blocking JS import not listed will execute synchronously, re-entering the WASM module in a state where ASYNCIFY's stack unwinding assumes it is not re-entrant, producing silent memory corruption.
Emscripten's Fetch API provides asynchronous HTTP requests from WASM: emscripten_fetch_t* fetch = emscripten_fetch(&attr, url) with attr.requestMethod, attr.onsuccess/attr.onerror callbacks, and attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY downloads the response body into Emscripten's heap accessible from C. For the MODULARIZE=1 factory pattern, the module initialization sequence is: const mod = await MyModule(); mod._calculate_option_price(...) — the factory returns a Promise that resolves when the WASM binary is downloaded, compiled, and the start function (if any) completes. Calling exported C functions via mod.ccall('function_name', 'number', ['number', 'number'], [arg1, arg2]) handles JS-to-WASM type coercion; mod.cwrap('function_name', 'number', ['number', 'number']) returns a JS function that wraps the coercion for repeated calls. Passing strings requires manual UTF-8 marshaling: const ptr = mod._malloc(str.length + 1); mod.stringToUTF8(str, ptr, str.length + 1); mod._process_string(ptr); mod._free(ptr) — failing to free the allocated pointer is the most common source of WASM heap leaks in Emscripten integrations.
WASI, wasmtime, wasmer, the component model, and server-side WASM runtimes
WASI (WebAssembly System Interface) provides a capability-based POSIX-like ABI for running WebAssembly outside the browser. The current stable ABI, wasi_snapshot_preview1, defines a set of host-imported functions that a WASM module can call for file I/O (fd_read, fd_write, fd_seek, fd_close, path_open), clock access (clock_time_get), process exit (proc_exit), and environment variable access (environ_get, environ_sizes_get). Building a WASI-compatible WASM binary uses the wasi-sdk toolchain: clang --target=wasm32-wasi -o output.wasm source.c links against wasi-libc, which implements the C standard library in terms of wasi_snapshot_preview1 host imports. The binary can be executed with wasmtime run --dir=. output.wasm — the --dir=. flag grants the WASM module capability access to the current directory; without it, any path_open call fails with EACCES. The capability model is WASI's security property: a WASM module has no access to the filesystem, network, or environment unless the host runtime explicitly grants it. Retainer work designing WASI modules for server-side execution focuses on mapping application I/O patterns to the minimum required capabilities and validating that the module fails correctly under capability denial.
Wasmtime's Rust embedding API is the most common production deployment for server-side WASM. The core objects form a hierarchy: Engine holds compilation configuration (optimization level, fuel metering for execution limits, epoch interruption for cooperative multitasking); Store<T> holds instance state and owns all WASM objects — it is parameterized on a host data type T that is accessible to host functions via Caller<T>; Module is a compiled WASM binary, created from bytes with Module::from_binary(&engine, bytes); Linker<T> resolves imports by name, adding host functions with linker.func_wrap("env", "log_i32", |caller: Caller<T>, val: i32| { ... }) and WASI imports with wasmtime_wasi::add_to_linker(&mut linker, |s| &mut s.wasi_ctx); Instance is the instantiated module, from which exported functions are retrieved with instance.get_typed_func::<(i32, i32), i32>(&mut store, "add")?. Fuel metering — configured with engine.config().consume_fuel(true) and store.set_fuel(1_000_000) — terminates execution after a configured number of WASM instructions, preventing unbounded computation in multi-tenant serverless deployments. The wasmer runtime provides a similar API with different lifetimes and a Module/Store/ImportObject pattern; retainer work covering portability between wasmtime and wasmer focuses on the import resolution differences and the Store lifetime semantics.
The WASI component model is the next generation of the WASI ABI, designed around composable components with typed interfaces defined in WIT (WebAssembly Interface Types). A WIT world definition names the imports a component needs and the exports it provides: world image-processor { import wasi:io/streams@0.2.0; export process: func(input: list<u8>) -> list<u8>; }. The wit-bindgen tool generates host and guest bindings from WIT definitions for Rust, C, and other languages — the guest sees a Rust trait to implement, the host sees a typed interface to call. The component model's canonical ABI handles memory ownership transfer between components: list<u8> arguments cross the component boundary by copying into the callee's linear memory, with the callee responsible for freeing the copy after the call. This explicit ownership model eliminates the SharedArrayBuffer threading problems inherent in the preview1 shared-memory model — components communicate via typed interface calls rather than shared memory pointers. Retainer work designing component model interfaces focuses on granularity (large transfers are expensive due to copying; streaming via wasi:io/streams amortizes the cost) and on generating TypeScript host bindings with jco transpile for browser deployment.
SharedArrayBuffer threading, Atomics synchronization, and wasm-bindgen JS integration
WebAssembly multithreading requires SharedArrayBuffer, which requires specific HTTP response headers to enable the browser's cross-origin isolation mode: Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. Without both headers on the page response, typeof SharedArrayBuffer === 'undefined' in the browser, and any WASM module compiled with Emscripten's -pthread or wasm-pack's --target no-modules with rayon threading will fail to instantiate. Setting these headers requires server-side configuration — in Nginx: add_header Cross-Origin-Opener-Policy "same-origin" and add_header Cross-Origin-Embedder-Policy "require-corp" in the server or location block for the HTML page. Third-party resources (fonts, analytics, CDN images) embedded on the page must either serve Cross-Origin-Resource-Policy: cross-origin headers or be proxied — resources without CORP headers fail to load under COEP and break the page. Retainer work configuring COEP/COOP for a production site involves auditing every third-party resource in the Network tab for missing CORP headers, either replacing non-compliant resources or proxying them through the same origin.
The WebAssembly.Memory API with shared: true — new WebAssembly.Memory({ initial: 256, maximum: 256, shared: true }) — allocates a SharedArrayBuffer-backed linear memory accessible from both the JS main thread and Web Workers. The WASM module imports this shared memory and accesses it with standard load/store instructions; multiple workers can instantiate the same WASM module with the same shared memory for true shared-memory multithreading. Atomics.wait(Atomics.wait(int32View, index, expectedValue)) blocks the current thread until the value at index changes from expectedValue or the optional timeout expires — this is a blocking operation that cannot be called on the JS main thread (it throws a TypeError in the browser), only in Web Workers. Atomics.notify(int32View, index, count) wakes up to count threads blocked on Atomics.wait at the same index. The WASM threading model using these primitives — pthreads compiled via -pthread in Emscripten, which maps pthreads mutex/cond operations to Atomics.wait/Atomics.notify — requires that each worker running a WASM thread loads and instantiates the WASM module independently with the shared memory passed as an import. Emscripten's -pthread build handles this worker creation automatically via the generated JS glue; custom threading designs using wasm-bindgen-rayon require manual worker pool management with WorkerPool from the wasm-bindgen-rayon crate.
wasm-bindgen generates JavaScript and TypeScript bindings for Rust WASM modules, handling the type conversions between JS values and WASM value types that the WASM spec cannot express directly. The #[wasm_bindgen] attribute on a Rust function or struct generates JS glue: a pub fn process(input: &[u8]) -> Vec<u8> annotated with #[wasm_bindgen] generates a JS function that allocates a WASM memory buffer for the input slice, copies the JS Uint8Array into it, calls the WASM function, reads the output Vec's pointer and length from WASM memory, copies it into a new JS Uint8Array, and frees the WASM-side Vec. The --reference-types flag enables the reference types proposal, allowing externref values to pass through WASM without going through the integer handle table and eliminating the associated JS-side handle map overhead. The --weak-refs flag enables JS WeakRef finalization for WASM-heap objects exposed to JS, ensuring that forgetting to call obj.free() on a wasm-bindgen struct from JS does not permanently leak the backing WASM memory. TypeScript declarations are generated with wasm-pack build --target web, producing a .d.ts file alongside the JS glue that provides full type coverage for the exported functions and structs — essential for TypeScript-based frontend codebases where the WASM module is called from typed application code.
How HourTab tracks WebAssembly developer retainer hours
WebAssembly developer retainers produce some of the most disproportionate work-to-visible-output ratios of any platform retainer. A session that diagnosed a SharedArrayBuffer thread synchronization failure produced a two-line Nginx configuration change. The session involved checking the browser console for the SharedArrayBuffer is not defined error and the Network tab for COEP violations on third-party fonts, identifying the missing Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers on the page response, locating the three CDN image resources that lacked Cross-Origin-Resource-Policy headers and would fail under COEP enforcement, proxying those three resources through a /assets-proxy/ Nginx location block rather than replacing them, and verifying that typeof SharedArrayBuffer !== 'undefined' on the main thread and that Atomics.wait executed without TypeError in the worker context. The log entry “fixed threading, 14h” gives the client no path from 14 hours to the production COEP/COOP configuration that unblocked the threaded WASM deployment — because nothing in two Nginx headers communicates the resource audit and proxy setup that produced them.
HourTab gives WebAssembly developers a public retainer-hours URL they share with each client at the start of the engagement. The client opens the URL and sees the current burn-down without logging in. For WASM retainers specifically, the work log format carries the weight: each entry should name the Emscripten flag and the binary size change it caused (-sASYNCIFY with ASYNCIFY_ONLY=['blocking_read'] — code size growth 2.3x instead of 3.8x; binary 2.1MB → 4.8MB instead of 8.0MB), the Binaryen wasm-opt pass and the size reduction (wasm-opt -O3 --low-memory-unused --strip-debug — 4.2MB → 1.8MB; dead code from unused SDL2 subsystems eliminated), the SharedArrayBuffer header pair and the third-party resource count audited (COEP/COOP headers added; 3 CDN resources proxied for CORP compliance; SharedArrayBuffer confirmed available, Atomics.wait confirmed unblocked in worker), the wasmtime fuel limit and the invocation it protected (store.set_fuel(2_000_000) — untrusted user-supplied WASM capped at 2M instructions; execution time bounded at ~8ms observed in profiling), and the wasm-bindgen flag and the overhead it eliminated (--reference-types --weak-refs — integer handle table eliminated; 12 externref values pass through WASM without JS heap allocation; WeakRef finalization prevents leak on uncalled .free()). That entry takes five minutes to write and turns the client check-in from a twenty-minute explanation of what COEP means and why Atomics.wait throws on the main thread into a two-sentence acknowledgment of the thread count working in production.
The retainer model fits WebAssembly platform engineering because the toolchain and proposal landscape evolves continuously — new Emscripten releases change flag semantics and default optimization behaviors, new WASM proposals (relaxed SIMD, component model, wasi-threads) reach production browser support and require toolchain adoption decisions, and new Binaryen optimization passes change the size/speed tradeoffs for existing binaries. A project contract closes when the current ASYNCIFY instrumentation or binary size optimization is complete. A WebAssembly retainer stays open for the next Emscripten version that deprecates a relied-upon flag, the next WASI proposal that enables a capability the application needs, and the next COEP/COOP header requirement that breaks a third-party resource integration.
Track WebAssembly developer retainer hours without the status emails
HourTab gives WASM engineers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: WebAssembly developer retainers
What does a WebAssembly developer on retainer typically do?
A WebAssembly developer on monthly retainer provides ongoing WASM platform advisory across binary format and WAT authoring (module sections, value types i32/i64/f32/f64/v128, SIMD v128 relaxed SIMD, reference types externref/funcref, bulk memory instructions memory.copy/memory.fill), Emscripten toolchain configuration (-sUSE_SDL=2/-sFETCH/-sASYNCIFY/-sMODULARIZE/-sALLOW_MEMORY_GROWTH, ASYNCIFY_IMPORTS/ASYNCIFY_ONLY/ASYNCIFY_LAZY_LOAD_CODE, ccall/cwrap string marshaling), WASI and server runtimes (wasi_snapshot_preview1 fd_read/fd_write/path_open, wasmtime Store/Engine/Linker/fuel metering, wasmer runtime, WIT interface types and wit-bindgen component model, wasi-threads), JavaScript integration (WebAssembly.Memory SharedArrayBuffer, COEP/COOP headers for SharedArrayBuffer enablement, Atomics.wait/Atomics.notify thread synchronization, wasm-bindgen --reference-types/--weak-refs TypeScript declarations), and binary optimization (wasm-opt -O3 --low-memory-unused --strip-debug Binaryen passes, dead code elimination from unused Emscripten ports, ASYNCIFY code size overhead analysis).
What WebAssembly work is most underlogged in a retainer?
ASYNCIFY instrumentation overhead reduction (tracing SDL_Delay transitive call through call graph via wasm-dis; adding ASYNCIFY_ONLY to limit instrumentation to 2 functions; code size growth 3.8x → 2.3x; 12–20 hours invisible in binary size number), SharedArrayBuffer COEP/COOP configuration (auditing 3rd-party resources for missing CORP headers; proxying non-compliant resources; verifying Atomics.wait unblocked in worker context; 8–16 hours invisible in thread count working), and wasm-opt binary size reduction (running Binaryen -O3 --low-memory-unused on 4.2MB Emscripten output; dead code from unused SDL2 subsystems eliminated; 1.8MB result; 4–8 hours invisible in the binary size metric) are the three most systematically underlogged WASM categories. Each produces a small diff — a flag list, two Nginx headers, or one wasm-opt command — representing large toolchain investigations that only surface in bundle size reports, threading failures, or browser console errors.
What are typical WebAssembly developer retainer rates?
Entry-level WebAssembly developers (1–3 years, basic Emscripten flags, simple wasm-bindgen, basic WASI file I/O) bill at $95–$165/hr. Mid-level WebAssembly engineers (3–7 years, ASYNCIFY instrumentation, SharedArrayBuffer COEP/COOP, wasm-opt optimization passes, Rust wasm-pack, wasmtime embedding) bill at $150–$265/hr. Senior WebAssembly architects (7+ years, WASI component model WIT interface types, Binaryen IR custom passes, SIMD v128 relaxed SIMD, multi-runtime wasmtime/wasmer/V8 portability) bill at $215–$395/hr. Firm rates run $175–$310/hr. Monthly retainer amounts: $3,500–$8,000/mo for advisory (15–30 hrs), $10,000–$25,000/mo for full WASM platform consulting.
What should a WebAssembly developer retainer agreement include?
A WebAssembly developer retainer agreement should specify toolchain scope (Emscripten emcc flag configuration, ASYNCIFY_IMPORTS/ASYNCIFY_ONLY/ASYNCIFY_LAZY_LOAD_CODE, wasi-sdk clang --target=wasm32-wasi, wasm-pack for Rust, Binaryen wasm-opt passes), WASI and runtime scope (wasi_snapshot_preview1 ABI, wasmtime Store/Engine/Linker/fuel metering, wasmer runtime, WIT interface types and wit-bindgen component model), JS integration scope (SharedArrayBuffer COEP/COOP headers, Atomics.wait/Atomics.notify, wasm-bindgen --reference-types/--weak-refs, WebAssembly.Memory growth, emscripten MODULARIZE=1), binary optimization scope (wasm-opt -O3 --low-memory-unused, ASYNCIFY overhead analysis, dead code elimination, --strip-debug/--strip-producers), and hour logging specifics (Emscripten flag and binary size change, Binaryen pass and size reduction, SharedArrayBuffer header pair and resource audit count, wasmtime fuel limit and invocation, wasm-bindgen flag and overhead eliminated).
How should WebAssembly developer retainer hours be logged?
Log each WebAssembly retainer session with: advisory category (Emscripten flag audit for -sASYNCIFY/-sMODULARIZE/-sALLOW_MEMORY_GROWTH, ASYNCIFY instrumentation with ASYNCIFY_IMPORTS/ASYNCIFY_ONLY/ASYNCIFY_LAZY_LOAD_CODE, SharedArrayBuffer COEP/COOP header configuration, Atomics.wait/Atomics.notify synchronization, WebAssembly.Memory growth and shared threading, wasm-bindgen --reference-types/--weak-refs TypeScript declarations, wasm-opt Binaryen -O3/--low-memory-unused/--strip-debug pass selection, WASI fd_read/fd_write/path_open capability design, wasmtime Store/Engine/Linker embedding, wasmer runtime configuration, WIT world interface types and wit-bindgen code generation, WAT binary format section analysis, wasm-pack Rust toolchain build configuration), specific module/function, diagnostic tool and output (wasm-objdump -h: code section 900KB of 1.2MB binary; wasm-dis disassembly: SDL_Delay reachable from 847 functions — ASYNCIFY_ONLY cannot scope below 800 functions; browser DevTools Network: COEP violation on 3 CDN font resources; wasmtime --fuel: trap after 2M instructions at 8ms average), fix applied and rationale (ASYNCIFY_LAZY_LOAD_CODE=1 — unwinding code deferred to runtime fetch, initial parse cost reduced 40%; 3 CDN fonts proxied via /assets-proxy/ with CORP: cross-origin — COEP violations resolved; wasm-opt -O3 --low-memory-unused applied — 4.2MB → 1.8MB, dead SDL2 subsystem code eliminated), scope (12 emcc -sPORT flags audited for unused system library includes; 6 ASYNCIFY_IMPORTS functions traced for async reachability; 3 Binaryen passes profiled for -O2/-O3/-Os size/speed tradeoff), before/after metric (binary size: 4.2MB → 1.8MB; ASYNCIFY code size growth: 3.8x → 2.3x; initial WASM parse time: 340ms → 200ms; SharedArrayBuffer: undefined → available, Atomics.wait: TypeError on main thread → blocked in worker correctly), and hours.