Buffered vs Streaming
typed-xlsx ships two workbook builders. Both support the same schema kinds (report and excel-table), the same formula columns, the same column groups, and the same output targets. The difference is in how row data flows through memory.
Buffered workbook
createWorkbook() holds all rows in memory while building. You pass rows: T[] at the table level and call an output method once. The builder plans every row, computes summaries, resolves formulas, and assembles the XLSX in one pass.
import { createExcelSchema, createWorkbook } from "@chronicstone/typed-xlsx";
const invoiceSchema = createExcelSchema<{
invoiceId: string;
customer: string;
amount: number;
dueDate: string;
}>()
.column("invoiceId", { header: "Invoice #", accessor: "invoiceId" })
.column("customer", { header: "Customer", accessor: "customer" })
.column("amount", { header: "Amount", accessor: "amount", style: { numFmt: "$#,##0.00" } })
.column("dueDate", { header: "Due Date", accessor: "dueDate" })
.build();
const rows = [
{ invoiceId: "INV-001", customer: "Acme Corp", amount: 4200, dueDate: "2025-03-31" },
{ invoiceId: "INV-002", customer: "Globex Inc", amount: 8750, dueDate: "2025-04-15" },
];
const workbook = createWorkbook();
workbook.sheet("Invoices").table("invoices", { schema: invoiceSchema, rows });
const buffer = workbook.toBuffer();
Use buffered when:
- The full dataset fits comfortably in memory (rule of thumb: under ~100k rows)
- You need summary rows (report mode) — their
step()reducers run as rows are planned - You want the simplest API with no commit lifecycle to manage
Streaming workbook
createWorkbookStream() writes rows to a spool (temporary file on disk) in batches. No rows are held in memory after a commit. The XLSX file is assembled by streaming the spool contents into a ZIP writer.
import { createExcelSchema, createWorkbookStream } from "@chronicstone/typed-xlsx";
const orderSchema = createExcelSchema<{
orderId: string;
sku: string;
qty: number;
unitPrice: number;
}>()
.column("orderId", { header: "Order ID", accessor: "orderId" })
.column("sku", { header: "SKU", accessor: "sku" })
.column("qty", { header: "Qty", accessor: "qty" })
.column("unitPrice", {
header: "Unit Price",
accessor: "unitPrice",
style: { numFmt: "$#,##0.00" },
})
.build();
const workbook = createWorkbookStream();
const table = await workbook.sheet("Orders").table("orders", { schema: orderSchema });
// Commit in batches — memory stays flat
await table.commit({ rows: [{ orderId: "ORD-1", sku: "SKU-A", qty: 5, unitPrice: 29.99 }] });
await table.commit({ rows: [{ orderId: "ORD-2", sku: "SKU-B", qty: 2, unitPrice: 149.99 }] });
await workbook.writeToFile("./orders.xlsx");
Use streaming when:
- You are exporting hundreds of thousands of rows
- Rows come from a database cursor or paginated API
- You want to keep Node.js heap usage flat regardless of dataset size
API shape differences
createWorkbook() | createWorkbookStream() | |
|---|---|---|
| Row input | rows: T[] on .table() | .commit({ rows }) on the table handle |
Sheet .table() return | WorkbookSheet (chainable sync) | Promise<WorkbookTableStream> |
| Finalize | .toBuffer() / .toUint8Array() | .writeToFile() / .pipeToNode() etc. |
| Summary rows | Yes | Yes (reducers accumulate across commits) |
| Excel table mode | Yes | Yes |
| Column groups | Yes | Yes |
| Formula columns | Yes | Yes |
Memory model
The buffered builder holds a PlannerResult for every table in memory until an output method is called. For a table with 100k rows and 20 columns, this is roughly the row data plus per-cell metadata — typically several hundred MB.
The stream builder writes each committed batch to a spool file and discards the in-memory representation. Peak memory is proportional to the largest single .commit() batch, not the total row count. A 1M-row export committed in batches of 1000 rows uses the same peak memory as a 1000-row export.
Summary reducers (init / step / finalize) work identically in both builders — step() is called once per row in each batch, accumulating into a tiny in-memory state object regardless of total row count.