Performance
What to expect, per runtime: WebAssembly overhead on browser and Node, native speed on mobile, plus bundle size and the settings that affect it.
tools/bench suite.
Browser & Node (WebAssembly)
Every figure here is a real measurement, taken by driving gdal3.js itself: in Node (the wasm-bundle) and in each browser engine (a small page that loads the library, mounts a file and times the call). Native is the GDAL CLI. The dataset is a 2048×2048 single-band GeoTIFF (4 MP) and a 50k-point vector; a real RGB or multi-band image would scale these up. Median of repeated runs. Full per-size and per-engine data: the bench suite.
Per operation
One table per operation, every platform as a row. gdal3.js here is the default multi-threaded build (the one the bundler plugins ship); native is the GDAL CLI. Median milliseconds.
Reproject a raster (warp), the only operation here that uses threads:
| PLATFORM | 1 THREAD | 2 THREADS |
|---|---|---|
| native GDAL | 58 ms | 35 ms |
| gdal3.js (Node) | 107 ms | 57 ms |
| gdal3.js (browser, Chromium) | 98 ms | 50 ms |
Note: a single-band 4 MP raster, shown at one thread then at NUM_THREADS=2. Of the operations here, warp gains the most from threads in the wasm build (see the threads note below).
Write a Cloud-Optimized GeoTIFF, by compression codec:
| PLATFORM | LZW | ZSTD | DEFLATE |
|---|---|---|---|
| native GDAL | 43 ms | 20 ms | 31 ms |
| gdal3.js (Node) | 46 ms | 23 ms | 31 ms |
| gdal3.js (browser, Chromium) | 44 ms | 22 ms | 28 ms |
Note: the same raster, three codecs that all ship in the wasm build. Choose by the usual size-versus-speed trade-off, exactly as you would natively.
Hillshade a DEM (gdaldem):
| PLATFORM | TIME |
|---|---|
| native GDAL | 20 ms |
| gdal3.js (Node) | 34 ms |
| gdal3.js (browser, Chromium) | 24 ms |
Note: a single-band 4 MP DEM, single-threaded on every platform.
Convert 50k features to FlatGeobuf (ogr2ogr):
| PLATFORM | TIME |
|---|---|
| native GDAL | 217 ms |
| gdal3.js (Node) | 186 ms |
| gdal3.js (browser, Chromium) | 173 ms |
Note: 50k random points. The wasm copy edges native here because ogr2ogr is allocation-heavy, where wasm's lean allocator and warm, long-lived module beat a freshly spawned CLI on the host's system allocator. That is platform-dependent, not a general "wasm is faster for vectors" result; compute-bound raster work still favours native.
- Threads. Two operations take
NUM_THREADS: warp and COG creation. Warp threads well in the wasm build, 107 to 57 ms; COG compression also parallelises, but the small pthread pool (~2) barely moves it (a few percent), where native COG with all cores nearly halves it. hillshade and ogr2ogr are single-threaded. See workers & threading. - Which build, st or mt? gdal3.js ships both; the tables use mt. The mt build can use threads but needs cross-origin isolation (COOP/COEP); the st build needs no special headers but cannot thread, and for non-parallel ops it lands within a few ms of mt. Pick mt if you can set the headers and want warp threading, st otherwise.
- Browser tracks Node. The same wasm, inputs held in memory (MEMFS); on a V8 engine it lands within a few ms of the Node row. SIMD is on by default, so the per-pixel math runs many lanes at once.
- Run in a Worker. Heavy work in a Web Worker (
useWorker: true) never blocks the UI, and the worker round-trip it adds is low single-digit percent, effectively free.
Engines do differ, sometimes by a lot, so treat per-engine absolutes as indicative; the browser bench records Chromium, Firefox and WebKit side by side.
iOS & Android (native)
On React Native, GDAL runs as a real native library over JSI, so there's no WebAssembly overhead and no browser memory ceiling. Expect native GDAL performance.
When you need maximum throughput
On a server where you can install native binaries, a native GDAL binding will be faster than wasm. If raw server throughput is the priority, reach for one.
Bundle size & first load
The full build is roughly 14 MB gzipped on first load (≈11.5 MB wasm + ≈2.2 MB driver/PROJ data), the price of the complete 189-driver registry. It loads lazily when you call initCppJs() and is cached after the first run. For size-capped targets, drop unused dependencies from the build or use the in-progress minimal driver-subset build.
Runtime tips
- One init, many jobs.
initCppJs()is the expensive call, keep the module alive and reuse it. - Use a worker for UX. Throughput is the same; your frame rate isn't. See workers.
- Multi-threaded build for big rasters, where GDAL parallelises (needs cross-origin isolation).
- Batch proxy reads. With
useWorker, each property read crosses the worker boundary, cache descriptors once. - OPFS over
/vsimem/for big inputs, keeps file bytes out of wasm memory (memory guide). - Close datasets, delete consumed outputs. Wasm memory doesn't shrink back.
Caching & offline
After the first visit the wasm and data come from HTTP cache; with a service worker (PWA) they're available entirely offline, there are no runtime calls to any server. Serve with compression (brotli) and far-future cache headers; the files are content-addressed by your bundler.