Skip to main content

Threading

cpp.js threading has two independent axes: runtime: 'st' | 'mt' (single vs multi-threaded Wasm) and useWorker: true | false (whether the Wasm module runs in a Web Worker). These are often confused — they solve different problems.

The two axes

runtime: 'st'runtime: 'mt'
useWorker: falseDefault. Main-thread Wasm. Smallest setup.Wasm runs main-thread; pthreads via SharedArrayBuffer. Needs COOP/COEP.
useWorker: trueWasm in 1 Web Worker. Comlink bridge. Required for OPFS persistence.Wasm in 1 Web Worker; pthreads spawn additional workers from there. Needs COOP/COEP.

Pick the right combination

What you wantPick
Quickest path to "C++ in browser"runtime: 'st', no useWorker
Persistent storage in browser (OPFS)runtime: 'st', useWorker: true
CPU-bound parallelism (image / geo / crypto)runtime: 'mt'
Both: persistent storage AND parallelismruntime: 'mt', useWorker: true
Cloudflare Worker / Deno Deploy / Vercel Edgeruntime: 'st' only — mt and useWorker not supported
React Nativeruntime: 'mt' if perf-sensitive (pthreads via JSI; no COOP/COEP needed)

Setting runtime: 'mt'

In cppjs.config.js:

export default {
general: { name: 'myapp' },
paths: { config: import.meta.url },
target: { runtime: 'mt' },
};

Two things happen at build time:

  1. The Wasm is compiled with -pthread (Emscripten flag).
  2. If any transitive dependency is mt, the project auto-promotes to mt. You can't downgrade an mt dep back to st.

The COOP/COEP requirement

Multi-threaded Wasm uses SharedArrayBuffer, which browsers gate behind cross-origin isolation. Your hosting layer must send these response headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Without them, SharedArrayBuffer is undefined and Wasm init silently fails. To verify in the browser console:

console.log(crossOriginIsolated) // must be true for `mt`
console.log(typeof SharedArrayBuffer) // must be 'function'

Per-host configuration

HostConfig
Vite dev / previewAuto-injected by @cpp.js/plugin-vite
Webpack / Rspack dev serverAuto-injected by @cpp.js/plugin-webpack
Vercelvercel.json headers entry (see snippet below)
Netlify_headers file (see snippet below)
Cloudflare Pages_headers file (same syntax as Netlify)
nginxadd_header directives (see snippet below)
Express / Next.js custom serverSet headers via middleware on every response

Vercel (vercel.json):

{
"headers": [{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
]
}]
}

Netlify / Cloudflare Pages (_headers):

/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

nginx:

add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;

COEP gotcha — third-party assets

require-corp blocks cross-origin resources unless they explicitly opt in (Cross-Origin-Resource-Policy: cross-origin on the response, or a crossorigin attribute on <img> / <script> tags). If your page loads third-party images / fonts / scripts, you have two options:

  • Switch to Cross-Origin-Embedder-Policy: credentialless (more permissive; supported in Chrome 96+, Firefox 110+).
  • Proxy third-party assets through your own origin.

useWorker: true (independent of threading)

Wasm runs in a single dedicated Web Worker; the main thread receives a Comlink-bridged proxy.

const m = await initCppJs({ useWorker: true });
// `m` looks identical, but every call is async.
const result = await m.add(2, 3);

Reach for useWorker: true when:

  • You need OPFS persistent storage (mandatory; OPFS is Worker-scope-only).
  • Your C++ is slow and you don't want to block the main thread.
  • You're using runtime: 'mt' and want pthread workers spawned from a non-main scope (cleaner architecture).

You don't need it when:

  • The C++ is fast (sub-frame) and main-thread blocking doesn't matter.
  • You're already using mt for parallelism (pthread workers are separate from useWorker).

What changes when useWorker: true

AspectWithout workerWith worker
m.add(2, 3) returns5Promise<5>
m.FS.writeFile(...) returnsundefinedPromise<undefined>
Synchronous callbacks from C++ to JSWorkDon't work — design as a promise round-trip
OPFS storageThrowsWorks (if browser supports)
Terminationn/ainitCppJs.terminate() kills the worker

Embind objects (vectors, structs) are auto-proxied via cpp.js's custom Comlink transfer handlers. m.toArray(vec) and m.toVector(cls, arr) work transparently across the boundary.

Edge runtime limits (Cloudflare Workers, Deno Deploy, Vercel Edge)

These platforms run JavaScript in V8 isolates that don't expose the Web Worker API. So:

  • useWorker: true — fails (no Worker constructor).
  • runtime: 'mt' — fails (pthreads need workers + SharedArrayBuffer).
  • OPFS — fails (browser-only API anyway).
  • runtime: 'st' + memory fs — works.

If your use case demands persistence on edge, you need an external service (R2, KV, S3) — call it from JS and feed bytes into Wasm via m.FS.writeFile(...).

React Native

Pthreads are routed through JSI (no SharedArrayBuffer, no COOP/COEP). runtime: 'mt' works without any host configuration. useWorker is a no-op (no Web Worker API in RN).

Common pitfalls

  1. mt works in dev but not in prod — the bundler plugin injects COOP/COEP for dev/preview, but production hosting needs explicit configuration. Look at crossOriginIsolated in console; if false, the headers are missing.
  2. Mixing mt and st artifacts in one bundle — they have incompatible memory layouts. The CLI prevents this at build time, but if you manually copy .wasm files between projects you'll see "wasm streaming compile failed" errors.
  3. Calling sync callbacks across useWorker: true — Comlink can't invoke main-thread sync code from a worker. If your C++ needs a JS callback, design it as a promise round-trip.
  4. Assuming runtime: 'mt' enables useWorker — they're independent. runtime: 'mt' without useWorker runs pthreads on the main thread; useWorker: true without runtime: 'mt' runs single-thread Wasm in a worker.
  5. Loading third-party scripts on a COEP pagerequire-corp blocks them unless they send Cross-Origin-Resource-Policy: cross-origin. Switch to credentialless or proxy.

See also