Docs · Getting started
In the browser
Run Vectro in a web page, on every core when the page is cross-origin isolated and with split tables when it isn't.
The browser build is vectrojs/browser. It has the same tables, expressions and SQL as Node, with three differences: queries that should use more than one core go through the …Async calls, threads need the page to be cross-origin isolated, and files come from fetch, file pickers and the browser’s own storage instead of paths.
import { Engine, sql } from 'vectrojs/browser';
const engine = new Engine();
const orders = await engine.loadCSV('/data/orders.csv'); // fetched, parsed on the workers
const eu = await orders.filter("region == 'EU'").aggAsync({ n: 'count()', revenue: 'sum(price * qty)' });
const rows = await sql('SELECT region, SUM(price) AS revenue FROM orders GROUP BY region', { orders }).toRowsAsync();
Serving the files
The engine starts its Web Workers from files next to browser.mjs (worker-browser.mjs, split-worker.mjs), found with new URL('./…', import.meta.url). Browsers only start workers from the page’s own origin, so those files have to be served by your site, not from a CDN on another domain.
- Without a bundler: copy
node_modules/vectrojs/srcinto your static folder and importbrowser.mjsfrom there. The files are plain ES modules. - With Vite: add
optimizeDeps: { exclude: ['vectrojs'] }so the dev server serves the package’s files as they are instead of pre-bundling them away from their worker files. Bundlers that understandnew Worker(new URL(…, import.meta.url))(Vite, webpack 5) copy the workers into the build.
Threads need cross-origin isolation
Workers on every core share one block of memory with the page, a SharedArrayBuffer. Browsers only allow that on pages served with these two headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
threadsAvailable() tells you whether the page has them. Without them, new Engine() still works, on the page’s thread alone, and needs no headers at all.
import { Engine, threadsAvailable } from 'vectrojs/browser';
console.log(threadsAvailable()); // true when crossOriginIsolated and SharedArrayBuffer exist
const engine = new Engine(); // workers on every core if so, one thread if not
How to send the headers depends on the host. A few common ones:
// vite.config.js: the dev server
export default {
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
optimizeDeps: { exclude: ['vectrojs'] },
};
# _headers (Netlify, Cloudflare Pages)
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
// Node's http module
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
Isolation has a cost: with require-corp, images, scripts and iframes from other origins only load if those servers send CORS or Cross-Origin-Resource-Policy headers. If your page embeds third-party content that doesn’t, isolation may not be an option; use split tables instead.
Use the async calls
A page’s main thread is not allowed to block and wait for other threads. So on an isolated page, the blocking calls (agg, toRows, groupBy().agg) still work but run on the page’s thread only; the …Async calls hand the work to the workers and keep the page responsive while they run.
const t = engine.table({ price, qty, region }); // making a table from arrays is quick either way
const total = await t.sumAsync('price * qty'); // every core
const groups = await t.groupBy('region').aggAsync({ revenue: 'sum(price * qty)' });
const rows = await groups.toRowsAsync();
groups.dispose();
const sameAnswer = t.sum('price * qty'); // the page's thread only
Every computing call has one: aggAsync, sumAsync, countAsync, deriveAsync, collectAsync, sortAsync, topAsync, joinAsync, toRowsAsync, parseCSVAsync, and toRowsAsync() on SQL queries and lazy chains. Several awaited queries on one engine are queued and run one after another.
The workers sleep as soon as a task is done instead of spinning in wait for the next one. Spinning only pays off when the caller blocks, as in Node; in Safari, seven spinning workers made queries four times slower and stalled the page.
Loading data
| Call | From |
|---|---|
engine.table(columns) |
Arrays and typed arrays already in the page |
await engine.loadCSV(url, opts) |
A URL, fetched and parsed on the workers |
await engine.parseCSVAsync(textOrBytes, opts) |
CSV text or bytes, e.g. from a File: new Uint8Array(await file.arrayBuffer()) |
engine.fromArrow(bytes) |
Arrow IPC bytes (.arrow, .feather, an Arrow stream) |
await engine.openDataset(source) |
A .vectro dataset from a URL, File or Blob, read a piece at a time |
There is no readCSV(path): pages have no file paths. See CSV, JSON lines, Arrow for CSV options.
Pages without isolation: SplitEngine
SplitEngine uses every core without shared memory, so it needs no headers. Each Web Worker runs its own engine and owns a share of every table’s rows. A query sends its expression to every worker; each one answers for its rows, and the page combines the small partial results: sums and counts are added, means are total sum over total count, groups are merged by key.
import { SplitEngine } from 'vectrojs/browser';
const engine = new SplitEngine(); // one worker per core
const orders = await engine.loadCSV('/data/orders.csv');
// or: await engine.tableAsync({ price, qty, region }), await engine.parseCSVAsync(text)
const eu = await orders.filter("region == 'EU'").aggAsync({ revenue: 'sum(price * qty)' });
const byRegion = await orders.groupBy('region').aggAsync({ revenue: 'sum(price * qty)' });
console.log(byRegion.toRows()); // a normal table on the page
await orders.dispose();
await engine.close();
Rows are dealt to the workers in blocks of 4,096, round-robin, rather than one range per worker. Real data is often in time order, and a filter like “the last month” then still finds its rows on every worker. Text is encoded on the page once, and only numbers travel to the workers, moved without copying.
The calls are the async ones of a normal table, so the same query code runs on either engine: filter, aggAsync, sumAsync and the other shortcuts, groupBy().aggAsync, deriveAsync, collectAsync, topAsync, toRowsAsync, toColumnsAsync, toArrowAsync. Not yet: joins, full sorts (topAsync only) and SQL. dispose() on a split table is async.
Choosing at run time
import { Engine, SplitEngine, threadsAvailable } from 'vectrojs/browser';
const engine = threadsAvailable() ? new Engine() : new SplitEngine();
const orders = engine instanceof SplitEngine
? await engine.tableAsync(columns)
: engine.table(columns);
// from here on, the same async calls work on both
const revenue = await orders.filter("region == 'EU'").sumAsync('price * qty');
If you need joins, sorts or SQL and can’t isolate the page, a plain new Engine() on one thread has all of them.
Datasets in the browser
Datasets open read-only from a URL, a File the user picked or dropped, or a Blob. From a URL they are read with HTTP range requests, so only the chunks and columns a query needs are fetched. A billion rows (a 5.1 GB file) open in a page in 51 ms, and “the last day” is answered in 24 ms reading 0.3 MB.
const orders = await engine.openDataset('/data/orders.vectro'); // or input.files[0]
await sql('SELECT region, COUNT(*) FROM orders GROUP BY region', { orders }).toRowsAsync();
Pages can also write datasets into the browser’s own file storage (OPFS). Bytes go to disk chunk by chunk, so a CSV bigger than memory converts too: a 246 MB CSV (5M rows) becomes a 24 MB dataset in 2.8 s.
await engine.csvToDataset(droppedFile, 'orders.vectro', { onProgress: (p) => console.log(p) });
await engine.writeDataset('eu.vectro', orders.filter("region == 'EU'"));
const stored = await engine.openStoredDataset('orders.vectro');
await engine.storedDatasets(); // [{ name, size }]
const file = await engine.storedDatasetFile('orders.vectro'); // a File, e.g. to download
await engine.removeStoredDataset('eu.vectro');
A FileSystemFileHandle from showSaveFilePicker() works as a target in place of a name. Huge group-bys, sorts and joins of two datasets spill to a temporary folder in browser storage, removed when the query ends.
Browsers
Tested in Chrome, Safari 26.5, Firefox 158 and Safari on iOS (a real iPhone). await engine.close() stops the workers when the page no longer needs them.