Large Exports
Most reports never need tuning. This page is for exports in the hundreds of thousands of rows, or for runtimes with a tight memory limit.
Start with the builder
createWorkbook() holds every row in memory and plans the whole sheet at once. That is the simplest API and fine for most reports.
Switch to createWorkbookStream() when the rows come from a cursor or a paginated API, or when holding them all at once is too much for your runtime. It accepts the same schema; only the row input changes.
import { createExcelSchema, createWorkbookStream } from "typed-xlsx";
type Order = { id: string; amount: number };
declare function pages(): AsyncIterable<Order[]>;
const schema = createExcelSchema<Order>()
.column("id", { accessor: "id" })
.column("amount", { accessor: "amount", summary: (s) => [s.formula("sum")] })
.build();
const workbook = createWorkbookStream();
const table = await workbook.sheet("Orders").table("orders", { schema });
for await (const page of pages()) {
await table.commit({ rows: page });
}
await workbook.writeToFile("./orders.xlsx");
Shared or inline strings
This is the setting that moves memory the most.
strings | What it does | Measured at 200k rows (see Benchmarks) |
|---|---|---|
"shared" (default) | one deduplicated string table, kept until finalize | 3.45 s, 415 MiB peak |
"inline" | strings written into each cell, no table in memory | 3.72 s, 274 MiB peak |
Use "inline", or memoryProfile: "low-memory", when memory is the constraint. Keep "shared" when the file size matters and many cells repeat the same strings (statuses, regions, categories).
import { createWorkbookStream } from "typed-xlsx";
const workbook = createWorkbookStream({ memoryProfile: "low-memory" });
Keep temporary data on disk
By default, committed rows are serialized to a temporary file and streamed into the ZIP at the end. Keep that default. tempStorage: "memory" holds the serialized XML in memory instead, and only makes sense when there is no writable filesystem.
Batch at your source's page size
Commit the pages your database or API already returns. Re-batching in your own loop adds work without saving memory. 1,000 to 10,000 rows per commit is a good range; the benchmarks use 5,000.
What still grows with the row count
Even with inline strings and file spooling, some features keep metadata for the whole export:
- merged cells from sub-row expansion
- hyperlinks and embedded images
- summary formulas that reference logical rows (
column.rows()) - shared strings, if you keep the default
None of them stop an export from streaming; they make memory grow slowly with the row count instead of staying flat.
Plan for sub-row expansion
An accessor that returns arrays multiplies physical rows. 100k orders with five lines each is a 500k-row sheet, and Excel stops at 1,048,576 rows per sheet. Split very large expanded exports across sheets.
Checklist
createWorkbookStream()for cursors, pagination, or exports that don't fit in memorymemoryProfile: "low-memory"when memory is the limit, the default when file size is- default file spooling, unless there is no writable filesystem
- commit your source's natural page size
- fixed
widthinstead ofautoWidthon very wide schemas, so widths aren't measured from every cell
Option details: Memory Tuning.