Compiling C and C++ to WebAssembly with Emscripten
{# Maintainer note: Sections 1, 2, and 3 of this guide are synced with the upstream Emscripten “Best Practices” documentation:
- Published page: https://emscripten.org/docs/compiling/best_practices.html
- Upstream source: site/source/docs/compiling/best_practices.rst in https://github.com/emscripten-core/emscripten When updating upstream Emscripten best practices, keep Sections 1-3 aligned. #}
Use Emscripten (emcc and em++) to compile C and C++ into high-performance
WebAssembly (Wasm) modules for the browser.
1. Recommended Emscripten Compilation Flags
Section titled “1. Recommended Emscripten Compilation Flags”{# Source: “Recommended Flags” in https://emscripten.org/docs/compiling/best_practices.html #}
When compiling and linking with emcc or em++, use modern modular flags:
-sSTRICT: Opt into strict modern Emscripten behavior and disable deprecated or legacy compatibility features.-sEXPORT_ES6: Emit a standard ES6 module (import/export). This option automatically implies-sMODULARIZE, encapsulating generated code so it does not pollute the global namespace.-sENVIRONMENT=web(or-sENVIRONMENT=workerwhen running solely inside a Web Worker, or-sENVIRONMENT=web,workerfor both): Strip Node.js and shell runtime support code to minimize bundle size.-sALLOW_MEMORY_GROWTH: Allow the WebAssembly linear memory heap to expand dynamically when allocations exceed the initial heap size.-Werror -Wall: Treat compiler warnings as errors to catch C/C++ bugs and invalid compiler settings early.-O3,-Os, or-Ozwith-flto: In release builds, choose-O3when runtime performance is most critical, or-Os/-Ozwhen minimizing binary payload size is the priority. Pass-fltoduring both compilation and linking for Link-Time Optimization.
Command-Line Style
Section titled “Command-Line Style”{# Source: “Command-Line Style” in https://emscripten.org/docs/compiling/best_practices.html #}
Keep emcc and em++ invocations clean, minimal, and consistent:
- Omit settings that are enabled by default: Do not pass redundant flags
that are already default in modern Emscripten, such as
-sWASM, or-sMODULARIZEwhen-sEXPORT_ES6is already specified. - Omit the
=1suffix on boolean-sflags: Write-sSTRICT,-sEXPORT_ES6, and-sALLOW_MEMORY_GROWTHrather than-sSTRICT=1,-sEXPORT_ES6=1, or-sALLOW_MEMORY_GROWTH=1. - Prefer standard compiler flags over Emscripten-specific ones: Use
-pthreadinstead of-sUSE_PTHREADS, and-m64instead of-sMEMORY64. - Use simple comma-separated lists: For list-based settings, pass
comma-separated values without brackets or quotes (for example,
-sEXPORTED_FUNCTIONS=_render,_malloc,_freeand-sEXPORTED_RUNTIME_METHODS=HEAPU8) rather than JSON arrays like-sEXPORTED_FUNCTIONS=['_render','_malloc','_free']. - Use response files for long lists: If an export list is large, pass a
response file (
@filename) instead of a long inline list (for example,-sEXPORTED_FUNCTIONS=@exported_funcs.txt). - Use separate compilation for multi-file projects: Compile
.c/.cppsources to.oobject files (em++ -c) before linking them into the final module rather than combining everything into a single monolithic invocation.
# Compile C++ source to an object fileem++ -c renderer.cpp -o renderer.o -O3 -flto -Werror -Wall
# Link into an ES6 WebAssembly module targeting a Web Workerem++ renderer.o -o renderer.js \ -O3 -flto \ -sSTRICT \ -sEXPORT_ES6 \ -sENVIRONMENT=worker \ -sALLOW_MEMORY_GROWTH \ -sEXPORTED_FUNCTIONS=_render_frame,_malloc,_free \ -sEXPORTED_RUNTIME_METHODS=HEAPU8Debug vs. Release Profiles
Section titled “Debug vs. Release Profiles”{# Source: “Debug vs. Release Profiles” in https://emscripten.org/docs/compiling/best_practices.html #}
Keep compile and link flags consistent within each build profile, and never ship debug information in production:
- Release builds: Use
-O3,-Os, or-Ozcombined with-fltoon both compile and link steps, and omit-gand-sASSERTIONS. Add--closure=1when linking to minify the generated JavaScript glue code unless an external bundler already minifies it. - Debug builds: Use
-gwhen compiling and-g,-gline-tables-only, or-gsource-mapwhen linking, alongside-sASSERTIONS. Avoid optimization flags (-O2,-O3,-Os,-Oz) and-fltoin debug builds for faster compilation and accurate source-level debugging.
2. JavaScript-WebAssembly Boundary Efficiency
Section titled “2. JavaScript-WebAssembly Boundary Efficiency”{# Source: “General Recommendations” in https://emscripten.org/docs/compiling/best_practices.html #}
Avoid high-frequency calls across the JavaScript-WebAssembly boundary. Calling an exported Wasm function per pixel, per audio sample, or per vertex incurs boundary overhead that negates Wasm performance gains.
Instead, design C/C++ entry points to process entire buffers or memory regions in a single call and read or write the contiguous buffer in linear memory:
import initRenderer from './renderer.js';
const wasm = await initRenderer();const byteLength = width * height * 4;const ptr = wasm._malloc(byteLength);
try { wasm._render_frame(ptr, width, height, samples); const pixels = new Uint8ClampedArray( wasm.HEAPU8.buffer, ptr, byteLength, ).slice(); self.postMessage({ pixels, width, height }, [pixels.buffer]);} finally { wasm._free(ptr);}Note: Accessing wasm.HEAPU8.buffer directly after calls that may grow memory
requires reading wasm.HEAPU8 fresh after the Wasm call returns, because
-sALLOW_MEMORY_GROWTH detaches the previous ArrayBuffer when resizing.
3. Avoid Blocking the Main Thread
Section titled “3. Avoid Blocking the Main Thread”{# Source: “Common Pitfalls -> Asynchronous Code Execution and Main Thread Blocking” in https://emscripten.org/docs/compiling/best_practices.html #}
The browser relies on cooperative multitasking on the main UI thread. Any WebAssembly call that takes more than a few milliseconds (such as image rendering, ray tracing, physics simulation, or heavy parsing) must not run synchronously on the main thread, or it will freeze page rendering and input.
Use one of these approaches to keep the UI responsive:
- Run the WebAssembly module in a Web Worker: Instantiate and call the
ES6 Wasm module inside a dedicated module Worker
(
new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })), and pass rendered buffers back viapostMessagewith transferableArrayBufferobjects. - Use multithreading (
-pthread): Offload heavy compute across background workers using standard POSIX threads. Note that-pthreadrequiresSharedArrayBuffer, which requires serving the page in a cross-origin isolated context (Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corp). - Yield to the browser event loop: Restructure continuous loops in C/C++
to run one frame at a time via
emscripten_set_main_loop, or use JavaScript Promise Integration (-sJSPI) / Asyncify (-sASYNCIFY) when synchronous C/C++ code must pause for asynchronous JavaScript APIs.
4. Test Runtime Execution and Memory Limits
Section titled “4. Test Runtime Execution and Memory Limits”Always run and verify the compiled WebAssembly module in a real browser environment and confirm that no warnings or unhandled errors appear in the console:
- Validate with
-sASSERTIONSduring development: Build and test with-sASSERTIONSenabled to catch invalid memory accesses, missing exports, or ABI mismatches early, then remove-sASSERTIONSfor the final release build. - Watch for stack overflows in recursive C/C++ code: Emscripten’s default
execution stack size (
-sSTACK_SIZE) is 64 KB, which is much smaller than native OS stacks. Deeply recursive algorithms (such as recursive ray tracers or tree traversals) can overflow the stack and corrupt linear memory unless converted to an iterative loop or linked with a larger stack (for example,-sSTACK_SIZE=1MB).
5. Avoid Dynamic Linking if Possible
Section titled “5. Avoid Dynamic Linking if Possible”Emscripten works best with static linking and static archive libraries (.a).
Shared libraries (.so, .dylib, .dll) and dynamic linking (-sMAIN_MODULE
/ -sSIDE_MODULE) add code-size and runtime overhead and should be avoided
unless runtime plugin loading is strictly required.
6. Porting Existing C/C++ Libraries and Build Integration
Section titled “6. Porting Existing C/C++ Libraries and Build Integration”When integrating C/C++ compilation into a web application project, wire the
emcc / em++ build into the project’s build pipeline (such as the "build"
script in package.json) so that building the project compiles the WebAssembly
artifacts before bundling.
When building third-party C/C++ libraries with existing build systems:
CMake Projects
Section titled “CMake Projects”Wrap cmake and make with emcmake and emmake:
mkdir -p build && cd buildemcmake cmake .. -DCMAKE_BUILD_TYPE=Releaseemmake makeAutoconf / Make Projects
Section titled “Autoconf / Make Projects”Use emconfigure to configure cross-compilation toolchain targets and disable
shared libraries:
emconfigure ./configure --host=wasm32-unknown-emscripten --disable-sharedemmake makeSource: GoogleChrome/modern-web-guidance-src (Apache-2.0), upstream commit 179dbc6, synced 2026-11-12. Local changes: macros expanded to links/badges, first heading de-duplicated, demo and this footer added. This is an independent, unofficial site — not a Google publication.