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. (Those runs used the optional shared-string
table, before inline strings became the default.) The whole browser entry is 29.0 KB brotli with no dependencies.
Streaming and memory has the numbers and the method.
The library is at 0.1 on npm, and the API may still shift before 1.0. CHANGELOG.md records every release.
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, under strings: 'auto', 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