typed-xlsx
Streaming

Streaming Intro

How createWorkbookStream() works, when to use it, and how to commit row batches.

createWorkbookStream() builds an XLSX file incrementally from row batches without ever holding the full dataset in memory.

Why streaming exists

The buffered builder (createWorkbook()) accumulates every row in memory before writing any XML. For small and medium reports this is fine. For large exports — hundreds of thousands of rows, wide schemas, multi-sheet reports — heap usage scales linearly with dataset size, and there is no way around it.

The stream builder separates ingestion from serialization. As you commit row batches, each batch is planned and serialized into an OOXML fragment that gets appended to a temporary spool (file-backed by default). When you call a finalization method, the engine streams the spooled sheet data into a ZIP archive incrementally — the compressor never loads the full workbook into memory at once.

mermaid
flowchart LR
  A[Buffered builder] --> B[All rows in memory]
  B --> C[Plan full sheet]
  C --> D[Serialize full XML]
  D --> E[Build ZIP in memory]

  F[Stream builder] --> G[One batch in memory]
  G --> H[Plan batch]
  H --> I[Serialize batch XML]
  I --> J[Append to spool]
  J --> K[Finalize ZIP as a stream]

When to use each builder

BufferedStream
Dataset sizeUp to ~50k rowsUnbounded
API styleSynchronous compositionAsync commit-based
Memory modelFull dataset in heapMostly bounded by batch size and config
Summary supportYesYes
Excel table modeYesYes
Formula columnsYesYes
Multi-table sheetsYesYes
Freeze panes, RTLYesYes
Output targetsBuffer, fileFile, Node stream, Web stream, Readable
ComplexityLowModerate

If your dataset fits comfortably in memory, use the buffered builder. The commit-based API adds async complexity; only reach for it when you actually need much flatter memory behavior.

For the exact memory trade-offs of tempStorage, tempDirectory, and string strategy, see Memory Tuning.

How the engine works

mermaid
flowchart TD
  A[createWorkbookStream] --> B[sheet Report]
  B --> C[table id plus schema]
  C --> D[commit batch 1]
  C --> E[commit batch 2]
  C --> F[commit batch 3]
  D --> G[plan plus serialize plus spool]
  E --> G
  F --> G
  G --> H[writeToFile or pipeToNode]
  H --> I[finalize summaries plus assemble ZIP]

Each commit call:

  1. Runs the row planner (sub-row expansion, merges, widths)
  2. Serializes the batch as OOXML <row> elements
  3. Appends the fragment to the sheet spool
  4. Advances each column's summary accumulator via step()

At finalization:

  1. Calls finalize() on each summary accumulator
  2. Appends summary rows to the spool
  3. Streams the ZIP: [Content_Types].xml, workbook.xml, styles.xml, each sheet spool, sharedStrings.xml
mermaid
flowchart LR
  A[Committed sheet spools] --> B[Finalize summaries]
  B --> C[Append summary rows]
  C --> D[Stream workbook parts into ZIP]
  D --> E[File or Node stream or Web stream]

Full feature parity

The stream builder is not a degraded mode. It supports the same feature set as the buffered builder:

  • Excel table mode — pass an ExcelTableSchemaDefinition and all native table features (autoFilter, style, totals row, structured formula refs) work identically to buffered mode
  • Formula columns — both A1-style (report mode) and structured references (excel-table mode) are resolved correctly at finalization time
  • Column summaries (reducer-based, accumulated per batch)
  • Sub-row expansion from array-valued accessors
  • Cell merges
  • Freeze panes (freezePane: { rows, columns })
  • Right-to-left sheets (rightToLeft: true)
  • Column selection (include / exclude)
  • Per-cell and per-header CellStyle
  • Multiple tables per sheet

Commit flow

Each stream table receives rows through successive .commit() calls:

import { 
createExcelSchema
,
createWorkbookStream
} from "typed-xlsx";
type
SaleRow
= {
date
: string;
region
: string;
revenue
: number;
}; const
schema
=
createExcelSchema
<
SaleRow
>()
.
column
("date", {
accessor
: "date",
header
: "Date" })
.
column
("region", {
accessor
: "region",
header
: "Region" })
.
column
("revenue", {
accessor
: "revenue",
header
: "Revenue",
style
: {
numFmt
: "#,##0.00" },
}) .
build
();
const
workbook
=
createWorkbookStream
({
tempStorage
: "file" });
const
sheet
=
workbook
.
sheet
("Sales", {
freezePane
: {
rows
: 1 } });
const
table
= await
sheet
.
table
("sales", {
schema
});
for (const
batch
of [[{
date
: "2024-01-01",
region
: "North",
revenue
: 4200 }]]) {
await
table
.
commit
({
rows
:
batch
});
} await
workbook
.
writeToFile
("./sales.xlsx");

For output methods after committing batches, see Output Targets.

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