Custom multi-threaded build
Build a pthread-enabled OCJS WASM with a trimmed symbol list or custom Emscripten flags.
The npm package already ships a pre-built multi-threaded variant at @taucad/opencascade.js/multi — see Package — Multi-threaded build for the import recipe, COOP/COEP headers, and OCCT activation calls. Rebuild from source only when you need:
- A trimmed symbol list (the shipped MT binary builds from
build-configs/full_multi.ymlwith the same ~4,400 symbols as the ST binary). - Custom
emccFlags(alternativeSTACK_SIZE,INITIAL_MEMORY, cappedPTHREAD_POOL_SIZE). - A relaxed-SIMD sibling for non-Safari deployments (see the callout below).
Build YAML
mainBuild:
name: occt_threaded
bindings:
# ... your symbol list ...
emccFlags:
- -O3
- -fwasm-exceptions
- -msimd128
- -pthread
- -sUSE_PTHREADS=1
# Pre-spawn one worker per logical CPU on the host. The expression is
# evaluated by the generated JS glue at module-instantiation time, so a
# single binary adapts to whatever hardware loads it.
- -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency
- -sSHARED_MEMORY=1
- -sMODULARIZE=1
- -sEXPORT_ES6=1
- -sENVIRONMENT=web,worker,node
- -sALLOW_MEMORY_GROWTH=1
- -sMAXIMUM_MEMORY=4GBBuild with:
./build-wasm.sh --config multi-threaded full build-configs/threaded.ymlPTHREAD_POOL_SIZE=navigator.hardwareConcurrency evaluates per-host: an M2
Pro browser gets 12 workers, a mobile device gets 4–6, a Node server gets
whatever os.cpus().length reports. Each worker costs STACK_SIZE (default
8 MB) at module init, so a 12-core box reserves ~96 MB of stack on top of
INITIAL_MEMORY. If you need a hard cap, wrap the expression:
Math.min(navigator.hardwareConcurrency, 16).
Why not a small fixed pool size?
A round-number pool like
-sPTHREAD_POOL_SIZE=4is tempting but silently caps your speedup. On a 12-core machine OCCT'sOSD_ThreadPoolrequests 11 workers (NbLogicalProcessors-1); with a pool of 4 the parallel sections degrade to 4-way fan-out and the headline speedup stalls around 1.3×. Sizing the pool tonavigator.hardwareConcurrencyrecovers the full 2.0–2.5× on mesh-dominated workloads, and shrinks gracefully on lower-core devices without a separate build.
Relaxed-SIMD — advanced, non-default
Do not add
-mrelaxed-simdto the binary you ship to browsers. Safari / WebKit (all versions as of May 2026) crash the JIT when a relaxed-SIMD opcode actually executes — validation passes but execution does not. If you want the 5–15 % uplift on Chromium / Firefox / Node, build a siblingocct_threaded_rsimd.wasmwith-mrelaxed-simdandwasm-opt --enable-relaxed-simd, then UA-sniff in your loader to serve the plain binary to WebKit and the relaxed binary to everyone else.
EVAL_CTORS=2is dropped under threading
full.ymlenables-sEVAL_CTORS=2for the single-threaded build, butfull_multi.ymldrops it — constructor evaluation order is non-deterministic under pthread workers. The trade-off is a slightly larger binary in exchange for race-free startup.Companion observation: the
wasm-optpost-link pass reports only ~0.1% size reduction on MT builds vs the 4-7% it achieves on ST. That is not a regression —wasm-optwould normally fold static ctor calls during the same passEVAL_CTORS=2enables; with that flag dropped, the constructors stay live and wasm-opt has less to eliminate. No action required.
Browser-only MT build
If you can guarantee your consumers run a recent browser (Chrome / Edge ≥ 144, Firefox ≥ 145, Safari ≥ 26.2 — May 2026 baseline) and you do not need to load the binary under Node.js, Bun, or Deno, you can opt into the multi-threaded-browser preset for a measurable runtime win on memory-growth-heavy workloads.
The preset layers -sGROWABLE_ARRAYBUFFERS=1 and -sENVIRONMENT=web,worker on top of the standard multi-threaded flag set. Internally Emscripten emits JS glue that wraps the pthread SharedArrayBuffer in a resizable ArrayBuffer view via WebAssembly.Memory.prototype.toResizableBuffer(). With that view in place, consumers can hoist HEAP* references out of hot loops — they no longer go stale after a memory-growth event, so the -Wpthreads-mem-growth link-time advisory disappears entirely and per-call HEAP re-fetches drop out of the inner loop.
Build with:
./build-wasm.sh --config multi-threaded-browser full build-configs/full_multi_browser.ymlThe shipped recipe lives at build-configs/full_multi_browser.yml; diff it against full_multi.yml to see the exact two-flag delta.
Runtime support matrix —
toResizableBuffer()(May 2026)Supported — ship
full_multi_browserartefacts to:
- Chrome / Edge ≥ 144
- Firefox ≥ 145
- Safari ≥ 26.2 (desktop + iOS)
NOT supported — fall back to plain
full_multifor:
- Node.js (all current LTS) — throws
TypeError: wasmMemory.toResizableBuffer is not a functionon module init.- Bun and Deno (all current versions).
- Samsung Internet and other Chromium derivatives that lag mainline.
Because Node.js is the dominant test-harness runtime, the default shipped multi-threaded build keeps
-sGROWABLE_ARRAYBUFFERS=0so it stays loadable undervitest,jest, and other Node-based test runners. Opt into the browser variant only for production payloads you serve directly to browser clients.
After building
Wire the resulting .js / .wasm triple into your bundler the same way as the shipped variant — see Package — Multi-threaded build for activation calls, parallel-aware OCCT APIs, and benchmarks.
Related
- Configurations reference —
multi-threadedpreset and how to author a custom one. - Custom emcc flags — link-time
emccFlagsrationale. - Trim symbols — shrink the symbol set before building.