typed-xlsx
Performance

Large Exports

How to keep big exports fast and memory-bounded, choose between buffered and streaming, shared and inline strings, and size your batches.

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.

stringsWhat it doesMeasured at 200k rows (see Benchmarks)
"shared" (default)one deduplicated string table, kept until finalize3.45 s, 415 MiB peak
"inline"strings written into each cell, no table in memory3.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 memory
  • memoryProfile: "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 width instead of autoWidth on very wide schemas, so widths aren't measured from every cell

Option details: Memory Tuning.

Copyright © 2026 Cyprien Thao. Released under the MIT License.