Worker & Streaming
Worker threading (≥ 20,000 rows)
At ≥ 20,000 rows in the browser, auto selects the Worker path: the main thread only does one structured clone (~94ms at 100k rows) and everything else happens inside the Worker. Rows 20,000–49,999 load WASM and build inside the Worker (Workbook path); at ≥ 50,000 rows it switches to the WASM-free Fast stream (see below).
The worker asset (export.worker.js, a self-contained ESM file) is located automatically — nothing to configure. Set configureWasm({ workerUrl }) only to point at a self-hosted copy.
Worker path behavior:
- Worker failures degrade gracefully — if the Worker route fails (e.g. a misconfigured
workerUrloverride 404s), the export retries on the main thread (modern-xlsx keeps styles; the ≥ 50,000-row fast stream needs no WASM at all). On the Worker + Workbook route a failing retry degrades further to the style-less stream fallback (mode: "stream"withresult.errorset, success staystrue). On the Worker + stream route (browser ≥ 50,000 rows) the retry is the fast stream on the identical input, so a failure there is terminal: the export resolves withsuccess: falseinstead of attempting a third build. Either way the caller's promise resolves rather than rejects, with an[excel-exporter]console warning at each degradation step; - The Worker instance is reused and requests are dispatched by
requestId, so concurrent exports never interfere; - Function-form formats are stripped (functions cannot be structured-cloned) — use FormatSpec on worker paths;
onProgress/onPhaseare forwarded from the Worker:buildonce the worker's build finishes, andinitonly on Worker + Workbook, when the worker actually initializes WASM — the Worker + stream route uses no WASM and reports noinitat all.
Streaming writes (≥ 50,000 rows)
fast-xlsx.ts uses fflate to produce minimal OOXML: ~0.8s at 100k rows (vs 17.5s on the Workbook path). auto selects it at ≥ 50k rows.
Known stream limitations:
| Feature | Stream path |
|---|---|
Multi-row headers (children) | supported (header auto-merge) |
Cell merges (merges) | supported (data area) |
Cell styles (style) | not supported |
Header styles (headerStyle) | not supported |
Column width (width) | not supported |
| Freeze / auto-filter | not supported |
| Custom number formats | not supported (decimals baked into stored value) |
| Date formats | readable strings per pattern |
| Progress callback | reported every 1000 rows |
Skipped features (cell styles, header styles, column width, freeze, ...) print [excel-exporter] stream mode: features not supported (...) in the console.
Which path should I use?
| Need | Recommended |
|---|---|
| < 50k rows with full styling | auto (main / worker + Workbook) |
| ≥ 50k rows, styling can degrade | auto (worker + Fast stream) |
| Large batch in Node | auto (main → stream at ≥ 50k) |
| Zero main-thread blocking | explicit mode: "worker" |
Lower-level APIs
WorkbookBuilder and exportAsStream are exported for fine-grained control:
import { WorkbookBuilder, exportAsStream } from "@marcusok/excel-exporter";
// batch build
const builder = await WorkbookBuilder.create();
builder.addSheet(sheetA).addSheet(sheetB);
const bytes = await builder.toBuffer();
// streaming
const { bytes, rowCount } = await exportAsStream(sheets, onProgress);WARNING
These lower-level entries only recognize an already-expanded __index__ column — unlike exportExcel, they do not expand SheetConfig.indexColumn themselves, so an indexColumn on a sheet passed to them directly is silently ignored. Call applyIndexColumn(sheet) first if you need the row-number column on these paths (the same helper exportExcel uses internally). See applyIndexColumn in the API reference for details.