simple-excel
@jetstreamapp/simple-excel reads and writes .xlsx files in browsers and in Node, with no runtime dependencies.
It writes a workbook by pushing rows into a sink as they arrive and reads one by pulling rows through an async
iterator. Nothing accumulates: there is no in-memory worksheet, no whole-sheet XML string and no unbounded shared
string table. A one-million-row export costs about as much memory as a one-thousand-row export — measured, that is
77 MB of JS heap growth to write 1,000,000 × 20 columns of Salesforce-shaped data, against 45 MB for 100,000 rows
of the same, where SheetJS throws RangeError: Invalid string length. The whole browser entry is 24.9 KB brotli
with no dependencies. Streaming and memory has the numbers and the method.
The library is pre-release and the API may still shift before 1.0. CHANGELOG.md records what has landed.
What it is for
Exporting and importing tabular data — query results, record loads, report extracts — in files that have to open cleanly in Excel and come back out carrying the same values.
import { collectToBlob, createWorkbookWriter } from '@jetstreamapp/simple-excel';
const sink = collectToBlob();
const workbook = createWorkbookWriter(sink);
const sheet = workbook.addSheet('Accounts', { header: ['Id', 'Name', 'Created'], freeze: { rows: 1 } });
await sheet.writeRow(['001xx000003DGb2AAG', 'Acme', new Date(2024, 2, 10)]);
await sheet.close();
await workbook.close();
const blob = await sink.result();
import { openWorkbook } from '@jetstreamapp/simple-excel';
const workbook = await openWorkbook(file);
for await (const row of workbook.sheet(0).rows({ mode: 'object' })) {
console.log(row.Id, row.Name);
}
await workbook.close();
When to pick it
Pick simple-excel when:
- the file is large, or you cannot predict how large it will be;
- it has to open in Excel without a repair prompt, and in Google Sheets, LibreOffice and Numbers too;
- you are running in a browser tab, a Web Worker or an extension service worker, where a multi-gigabyte heap spike ends the session;
- you want to know exactly what happens to a date, a 40,000-character cell or a control character, because the answer is written down;
- adding a dependency tree to a code path that touches untrusted uploads is not acceptable.
Pick something else when:
- you need to edit an existing workbook and preserve the parts you did not touch;
- you need formulas, charts, pivot tables, conditional formatting or data validation;
- you need to read
.xls,.xlsbor.ods(simple-excel detects each and says what it is, but does not parse it); - you need Excel's number formats rendered to display text.
Comparison has the library-by-library reasoning.
How the pieces fit
| Concept | What it is |
|---|---|
ByteSink | Where written bytes go: a Blob, a byte array, a WritableStream, a file. See Writing |
WorkbookWriter | Owns the zip, the styles and the shared strings; hands out one SheetWriter at a time |
SourceInput | Where read bytes come from: ArrayBuffer, Uint8Array, Blob/File, or a RandomAccessSource |
Workbook / Sheet | Lists sheets without touching sheet XML; streams rows on demand. See Reading |
XlsxError | Every failure, carrying a stable code. See Errors |
Where to go next
- Install — package, entry points, environment requirements
- Writing — sinks, sheets, rows, styles, options
- Reading — opening, listing sheets, array and object rows
- Dates and values — the value model and its edge cases
- Errors — every error code and when it fires
- Streaming and memory — how both sides stay flat, and what to do with very large files
- Compatibility — what was verified, against what
- Node — the
/nodeentry - Comparison — the longer version of the README table
- Contributing — repo layout, commands, fixtures