OpenCascade.js

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.yml with the same ~4,400 symbols as the ST binary).
  • Custom emccFlags (alternative STACK_SIZE, INITIAL_MEMORY, capped PTHREAD_POOL_SIZE).
  • A relaxed-SIMD sibling for non-Safari deployments (see the callout below).

Build YAML

build-configs/threaded.yml
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=4GB

Build with:

./build-wasm.sh --config multi-threaded full build-configs/threaded.yml

PTHREAD_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=4 is tempting but silently caps your speedup. On a 12-core machine OCCT's OSD_ThreadPool requests 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 to navigator.hardwareConcurrency recovers 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-simd to 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 sibling occt_threaded_rsimd.wasm with -mrelaxed-simd and wasm-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=2 is dropped under threading

full.yml enables -sEVAL_CTORS=2 for the single-threaded build, but full_multi.yml drops 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-opt post-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-opt would normally fold static ctor calls during the same pass EVAL_CTORS=2 enables; 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.yml

The 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_browser artefacts to:

  • Chrome / Edge ≥ 144
  • Firefox ≥ 145
  • Safari ≥ 26.2 (desktop + iOS)

NOT supported — fall back to plain full_multi for:

  • Node.js (all current LTS) — throws TypeError: wasmMemory.toResizableBuffer is not a function on 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=0 so it stays loadable under vitest, 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.

On this page