Skip to main content

Bundling into an application

Everything these packages need at runtime resolves through their exports maps, so a bundler finds it without configuration. There is exactly one thing an application has to wire up by hand: the URL of the parquet worker.

If you use @spatialdata/vis, that is one call:

import { ensureWorkers } from '@spatialdata/vis';
// Vite. See "Other bundlers" below.
import workerUrl from '@spatialdata/core/parquet-worker?worker&url';

ensureWorkers({ parquet: { workerUrl } });

Core on its own has no ensureWorkers; call enableParquetWorker({ workerUrl }) from @spatialdata/core instead.

The two workers​

WorkerDoesEnabled byNeeds a URL?
CodecZarr chunk decode — imagery, labelsensureCodecWorkers() (vis)No
ParquetParquet decode and scans — points, shapes, tablesenableParquetWorker() (core)Yes, from a bundled app

ensureWorkers() starts both. Every option is optional, so a bare ensureWorkers() is valid; pass false for either worker to leave it off, and per-worker objects to override defaults:

ensureWorkers({
codec: { chunkCacheMaxBytes: 512 * 1024 * 1024 },
parquet: { workerUrl, requestTimeoutMs: 120_000 },
});

It returns what is actually running — { codec, parquet } — which is not always what you asked for. Neither worker starts outside a browser, and the parquet worker reports false if it fails to load.

Both are idempotent, and the renderer paths call ensureCodecWorkers() for you, so the reason to call ensureWorkers() is the parquet worker — or wanting both up before any UI mounts.

Why only one of them needs a URL​

The codec worker ships as a single self-contained file, so your bundler can copy it as a static asset and rewrite the URL core hands it. That is why it needs no configuration.

The parquet worker cannot be built that way: it imports shared chunks and apache-arrow, and it loads a 6.6 MB parquet-wasm of its own. Copied as an asset it is 11 kB whose every import 404s; made self-contained it would carry an inlined copy of that wasm. So your bundler has to build it, and the URL import is what asks it to:

import workerUrl from '@spatialdata/core/parquet-worker?worker&url';

That import is doing the work — the string it yields is almost incidental.

Calling enableParquetWorker() with no URL is right only where core is loaded as published ESM without being re-bundled: a dev server serving node_modules, an import map, a CDN. There dist/parquet-worker.js really does sit next to the chunk asking for it. Once a bundler inlines core's chunk into its own output, it does not.

If you get it wrong​

A worker that fails to load is detected, switched off, and logged — core falls back to main-thread decoding rather than waiting for a request timeout. So a wrong URL costs performance, not correctness, with one exception:

loadPointsMatchingFeatureCodes — the feature-index scan that fetches a selected feature's points from beyond the resident memory window — has no main-thread fallback and throws without a working worker. Gate any UI that depends on it:

const { parquet } = ensureWorkers({ parquet: { workerUrl } });
if (parquet) {
// offer on-demand feature loading
}

isParquetWorkerEnabled() from core answers the same question later, and reports false after a failed startup rather than staying optimistic.

parquet-wasm: nothing to configure​

Core reads Parquet through a vendored parquet-wasm build, loaded from the @spatialdata/core/parquet-wasm export. Your bundler resolves that subpath, follows the glue's own new URL('parquet_wasm_bg.wasm', import.meta.url), and emits the wasm alongside your other assets — in both the main bundle and the worker's.

You should see something like assets/parquet_wasm_bg-B1vu2ek6.wasm in your build output. With default asset naming both graphs produce the same hashed filename, so that is one file, not two. If you have a custom assetFileNames (or webpack assetModuleFilename) routing .wasm somewhere unhashed, check you have not turned it into two copies of 6.6 MB.

Other bundlers​

BundlerHow to wire it
Vite / Rollupimport workerUrl from '@spatialdata/core/parquet-worker?worker&url', then ensureWorkers({ parquet: { workerUrl } }) — verified in CI against a real production build
webpackcreateWorker with a local entry file, below. This docs site does it, so every docs build exercises it.
OthersWhatever your bundler provides for building a worker from a module, then pass either a URL (workerUrl) or a factory (createWorker). Check the worker and a .wasm both appear in your output.
No bundlerNothing. ensureWorkers() with no options already resolves.

webpack, and createWorker​

webpack cannot hand you a URL for a bundled worker ahead of time: it only builds one when it can see the new Worker(new URL(...)) form literally. So give it a factory instead of a URL:

ensureWorkers({
parquet: {
createWorker: () =>
new Worker(new URL('./parquetWorkerEntry.ts', import.meta.url), { type: 'module' }),
},
});

That URL must point at a local file in your own source, which is one line:

// parquetWorkerEntry.ts
import '@spatialdata/core/parquet-worker';

Pointing it straight at the bare specifier — new URL('@spatialdata/core/parquet-worker', import.meta.url) — does not work. webpack resolves it to core's published dist/parquet-worker.js and emits that file as a static asset, still carrying its relative imports to sibling chunks and a bare apache-arrow: 9 kB whose every import 404s, the same shape of failure as copying the worker. Going through a local file makes webpack treat it as a worker entry and bundle the graph behind it.

createWorker takes precedence over workerUrl. It is a factory rather than a Worker because enabling tears down and rebuilds — an instance could only be used once.

Checking your build​

Two things worth asserting in CI, both cheap:

  • No emitted chunk references a file the build did not emit. A missing wasm or worker asset is invisible in dev and fatal in production.
  • The app actually starts the worker and decodes something. Our own tests/production/browser/parquet-worker.spec.ts does this against a real consumer build; it is the only place the published worker entry, its module format, and its private parquet-wasm resolution are all exercised at once.