Workers & threading
Two independent knobs: where GDAL runs (main thread or a Web Worker) and how the wasm was built (single- or multi-threaded).
useWorker: keep the UI thread free
const Module = await initCppJs({ useWorker: true });
With useWorker: true the wasm module boots inside a Web Worker and every object you touch (Gdal, datasets, drivers) is an async proxy. Calls hop to the worker, results hop back. A multi-gigabyte reprojection won't drop a frame in your UI. This is how the bundled converter app runs.
Practical consequence: everything is awaitable. Property-like reads are method calls returning promises; batch what you need (the converter caches driver descriptors once at boot for exactly this reason).
Single- vs multi-threaded builds
The wasm ships in two variants: st (single-threaded) and mt (multi-threaded, pthreads via SharedArrayBuffer). The multi-threaded build lets GDAL parallelise internally; the single-threaded build runs anywhere without special headers. Builds exist for browser, Node and edge targets. When threads share the virtual filesystem, OPFS is the safe backing store: its access handles are synchronous and take an exclusive per-file lock, so the platform serialises concurrent access rather than leaving threads to race.
The plugin builds the single-threaded variant by default. To opt into the multi-threaded build, add target: { runtime: "mt" } to your cppjs.config.js:
import wasm from "@gdal3.js/wasm/cppjs.config.mjs";
export default {
dependencies: [wasm],
paths: { config: import.meta.url },
target: { runtime: "mt" }, // default is "st"
};
That compiles the wasm with pthreads. In the browser the multi-threaded build then needs cross-origin isolation (below); React Native needs no headers (pthreads run over JSI); edge runtimes stay single-threaded.
By default the pool is small (cpp.js caps it at about two workers), so GDAL options that take a thread count, like -wo NUM_THREADS=ALL_CPUS, are bounded by it. Raising the pool is a build-level setting, covered in cpp.js's performance guide.
Cross-origin isolation (multi-threaded only)
SharedArrayBuffer requires the document to be cross-origin isolated. To use the multi-threaded build, serve your app with:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
The Vite plugin sets these in dev. In production it's a host setting (Netlify _headers, nginx add_header, etc.). The single-threaded build needs none of this; when in doubt, start there.