Confirmed Emscripten flags for Wasm SIMD, Relaxed SIMD, pthread pools, COOP/COEP headers, memory growth, allocators, and LTO. Read now.
SIMD and Relaxed SIMD
Vectorized code paths are opt-in at compile time. Passing -msimd128 tells the compiler and the LLVM backend to lower autovectorized loops and Wasm SIMD intrinsics to the fixed-width 128-bit instruction set, which every current engine understands. If you want the wider latitude of Relaxed SIMD, add -mrelaxed-simd; it unlocks operations whose exact rounding or lane behavior an engine may implement differently, trading strict determinism for speed on hardware that supports the faster forms.
Because Relaxed SIMD results can vary slightly between engines, keep it out of code paths that must produce bit-identical output across machines. A common pattern is to build one module with baseline SIMD and a second with Relaxed SIMD, then feature-detect at load time and pick whichever the running engine accepts.
Threads, pools, and the header requirement
Threading builds on -pthread, which routes pthreads calls onto Web Workers backed by a shared memory. Spawning a worker mid-computation is expensive, so pre-allocate them with -sPTHREAD_POOL_SIZE sized to your expected parallelism. A pool that is warm before the hot path starts avoids stalls when your code calls into pthread_create.
Shared memory only works when the page is cross-origin isolated, which the browser gates behind two response headers on the top-level document:
Cross-Origin-Opener-Policy: same-origin(COOP) — severs the page from other browsing contexts.Cross-Origin-Embedder-Policy: require-corp(COEP) — forces every subresource to opt in to being embedded.
Without both, SharedArrayBuffer is unavailable and threaded modules fail to start. Set them on your HTML and worker scripts, and make sure any cross-origin assets you pull in also send the matching CORP or CORS headers, or they will be blocked once COEP is on.
Memory growth and allocators
Threaded builds use a shared, growable memory, but growth has costs. Enable it with -sALLOW_MEMORY_GROWTH when you cannot predict peak usage; skip it and set a fixed initial size when you can, since a memory that never grows avoids the reallocation and re-wiring that growth triggers. Under threads, growing memory has to be coordinated across every worker, so err toward provisioning enough up front.
The default allocator is general-purpose. Workloads with heavy or highly concurrent allocation often do better with a different one selected through -sMALLOC, such as a multithread-friendly allocator that reduces lock contention. Measure your own allocation pattern before switching; the win depends entirely on how your code churns the heap.
Link-time optimization
Turn on LTO with -flto at both compile and link steps so the toolchain can inline and prune across translation units instead of one file at a time. This matters most for SIMD and threaded builds, where small hot kernels benefit from being inlined into their callers and dead specializations get stripped out.
LTO lengthens link time and interacts with your optimization level, so pair it with an explicit -O2 or -O3 and treat the final link as the build stage where flag choices actually get resolved. Keep the same SIMD, threading, and allocator flags consistent across compile and link — mismatched flags between the two phases are a frequent source of modules that build cleanly but behave unexpectedly at runtime.