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 mt build lets GDAL parallelise internally; the st build runs anywhere without special headers. Builds exist for browser, Node and edge targets.
Cross-origin isolation (mt only)
SharedArrayBuffer requires the document to be cross-origin isolated. To use the mt 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 st build needs none of this. When in doubt, start with st.
The worker leak fix
If you ran thousands of chunked jobs through the v2 worker and watched the tab's heap grow: that was a real bug: resolved promises were never released in the worker wrapper. Fixed by community PR #104 (April 2026). Long tile pipelines are safe on current builds.
Cancelling running work
There is no abort API yet (issue #91). The workaround is coarse: run disposable jobs in a worker you own, and tear the whole module down when results are no longer needed, re-initialising for the next job. Within SQL workflows, ds.abortSQL() exists at the dataset level.