Two orthogonal axes: threading (single vs multi-thread Wasm) and
useWorker(whether the Wasm module runs in a Web Worker). Don't confuse them.
│ runtime: 'st' │ runtime: 'mt'
────────────────────┼──────────────────────┼─────────────────────────
useWorker: false │ Default. Main thread │ Wasm runs main-thread,
│ Wasm. Smallest setup.│ pthreads via SharedArray-
│ │ Buffer. Needs COOP/COEP.
────────────────────┼──────────────────────┼─────────────────────────
useWorker: true │ Wasm in 1 Web Worker.│ Wasm in 1 Web Worker;
│ Comlink bridge. Main │ pthreads spawn ADDITIONAL
│ thread free. Required│ workers from there. Needs
│ for OPFS persistence.│ COOP/COEP + Worker support.
| You want | Pick |
|---|---|
| Quickest path to "C++ in browser" | runtime: 'st', no useWorker |
| Persistent storage in browser | runtime: 'st', useWorker: true |
| CPU-bound parallelism (image / geo / crypto) | runtime: 'mt' |
| Both: persistent storage AND parallelism | runtime: 'mt', useWorker: true |
| Cloudflare Worker / Deno Deploy / Vercel Edge | runtime: 'st' only — mt and useWorker not supported |
| React Native | runtime: 'mt' if perf-sensitive (pthreads via JSI; no COOP/COEP needed) |
In cppjs.config.js:
export default {
general: { name: 'myapp' },
paths: { config: import.meta.url },
target: { runtime: 'mt' }, // ← here
}Two things happen at build time:
- The Wasm is compiled with
-pthread(Emscripten flag). - Any transitive dependency that's already
mtkeeps the project onmt. Conversely, if any dep ismt, this project auto-promotes tomt(you can't downgrade).
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 the Wasm init silently fails.
console.log(crossOriginIsolated) // must be true for `mt`
console.log(typeof SharedArrayBuffer) // must be 'function'| Host | Config |
|---|---|
| Vite dev / preview | Auto-injected by @cpp.js/plugin-vite |
| Webpack / Rspack dev server | Auto-injected by @cpp.js/plugin-webpack |
| Vercel | Add to vercel.json: { "headers": [{ "source": "/(.*)", "headers": [{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }, { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }] }] } |
| Netlify | Add to _headers: /*\n Cross-Origin-Opener-Policy: same-origin\n Cross-Origin-Embedder-Policy: require-corp |
| Cloudflare Pages | _headers file (same syntax as Netlify) |
| nginx | add_header Cross-Origin-Opener-Policy same-origin; add_header Cross-Origin-Embedder-Policy require-corp; |
| Express / Next.js custom server | Set headers via middleware on every response |
require-corp blocks cross-origin resources unless they explicitly opt in (Cross-Origin-Resource-Policy: cross-origin on the response, or crossorigin attribute on <img> / <script> tags). If your page loads third-party images / fonts / scripts, you'll either need to:
- Switch to
Cross-Origin-Embedder-Policy: credentialless(more permissive, supported in Chrome 96+, Firefox 110+). - Or proxy third-party assets through your own origin.
Wasm runs in a single dedicated Web Worker; 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)You want this 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 paint loop.
- 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
mtfor parallelism (pthread workers are separate fromuseWorker).
| Aspect | Without worker | With worker |
|---|---|---|
m.add(2, 3) returns |
5 |
Promise<5> |
m.FS.writeFile(...) returns |
undefined |
Promise<undefined> |
| Synchronous callbacks | Work | Don't work — use returned promises |
| OPFS storage | Throws | Works (if browser supports) |
| Termination | n/a | initCppJs.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.
These platforms run JavaScript in V8 isolates that don't expose the Web Worker API. Therefore:
- ❌
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(...).
Pthreads are routed through JSI (no SharedArrayBuffer, no COOP/COEP). runtime: 'mt' works without any host configuration. useWorker is a no-op (n/a — no Web Worker API in RN).
mtworks in dev but not in prod — the bundler plugin injects COOP/COEP for dev/preview, but production hosting needs explicit configuration. Look atcrossOriginIsolatedin console; iffalse, the headers are missing.- Mixing
mtandstartifacts in one bundle — they have incompatible memory layouts. The CLI prevents this at build time but if you manually copy.wasmfiles between projects, you'll see "wasm streaming compile failed" errors. - Calling sync callbacks across
useWorker: true— Comlink can't invoke main-thread sync code from worker. If your C++ needs a JS callback, design it as a promise round-trip. - Assuming
runtime: 'mt'enablesuseWorker— they're independent.runtime: 'mt'withoutuseWorkerruns pthreads on the main thread;useWorker: truewithoutruntime: 'mt'runs single-thread Wasm in a worker. - Loading third-party scripts on a COEP page —
require-corpblocks them unless they sendCross-Origin-Resource-Policy: cross-origin. Switch tocredentiallessor proxy.
init.md—useWorker,runtime(viacppjs.config.js),getWasmFunction.cppjs-config.md—target.runtime: 'st' | 'mt'.filesystem.md— why OPFS depends onuseWorker.docs/playbooks/integration/*.md— per-framework COOP/COEP setup.