Skip to content

Performance

ABCrimson edited this page Mar 6, 2026 · 4 revisions

Performance

Benchmarks

All benchmarks run on Node.js, single thread (v0.9.1).

Operation modern-xlsx SheetJS CE Factor
Read 100K rows 472 ms 1,901 ms 4.0x faster
Read 10K rows 69 ms 170 ms 2.5x faster
Write 100K (batch aoaToSheet) 232 ms 1,950 ms 8.4x faster
Write 50K (batch aoaToSheet) 49 ms 80 ms 1.6x faster
Write 10K (cell-by-cell) 175 ms 125 ms 0.7x
sheetToCsv (10K) 37 ms 31 ms ~1.0x
sheetToJson (10K) 36 ms 22 ms ~0.6x

Summary: modern-xlsx is 4-8x faster for bulk read/write — its primary use case. SheetJS is faster for cell-by-cell writes and small utility conversions (sheetToJson, sheetToCsv). For large workbooks (10K+ rows), the WASM-accelerated Rust core delivers significant throughput gains.

Why It's Fast

Rust WASM Core

The heavy lifting — ZIP decompression, XML parsing, shared string table lookup, and style resolution — runs in compiled WASM at near-native speed. The WASM sandbox also provides memory safety guarantees.

JSON Bridge

Data crosses the WASM boundary as a JSON string (serialized with serde_json in Rust, parsed with JSON.parse in JS). This is 8-13x faster than serde_wasm_bindgen for large workbooks because:

  1. JSON serialization in Rust is heavily optimized (itoa, ryu)
  2. JSON.parse is one of the fastest built-in JS operations
  3. Avoids thousands of individual WASM boundary crossings

SAX-Style XML Parsing

The Rust core uses quick-xml in SAX (streaming) mode rather than building a DOM tree. This keeps memory usage proportional to the current element being processed, not the entire document.

Inline SST Remapping

When writing, the shared string table is built and indices are remapped inline during XML generation — avoiding a full worksheet clone that earlier versions required.

Optimization Tips

Batch APIs

Use aoaToSheet / jsonToSheet instead of setting cells one by one:

// Fast — batch API
const ws = aoaToSheet(data);

// Slower — individual cell access
for (const [r, row] of data.entries()) {
  for (const [c, val] of row.entries()) {
    ws.cell(encodeCellRef(r, c)).value = val;
  }
}

Limit Rows on Read

Use sheetRows to limit how many rows are parsed:

const first100 = sheetToJson(ws, { sheetRows: 100 });

Initialize Once

Call initWasm() once at startup, not before every operation:

// Good
await initWasm();
// ...later, many operations...

// Bad — redundant (safe but wasteful)
await initWasm();
const wb1 = await readFile('a.xlsx');
await initWasm(); // unnecessary
const wb2 = await readFile('b.xlsx');

Bundle Size

Component Size
WASM binary ~939 KB
JS wrapper ~55 KB
Total ~994 KB

The WASM binary is loaded asynchronously and can be cached by the browser/runtime. The JS code is tree-shakable — unused utilities are eliminated by bundlers.

v0.9.1 Optimizations

The v0.9.1 release focused on eliminating unnecessary allocations in hot paths:

ryu Float Formatting

All f64-to-string conversions in worksheet JSON serialization and chart value output now use the ryu crate, which provides 2-6x faster formatting than format!("{}", f) by using the Ryu algorithm (Ulf Adams, PLDI 2018).

Byte-Level JSON Escaping

The custom JSON string escaper was rewritten to batch-copy safe byte spans via memcpy instead of iterating character-by-character. This significantly reduces overhead when serializing large string values (shared strings, cell formulas).

Zero-Alloc Relationship Constants

Relationship fields now use Cow<'static, str>, allowing well-known OOXML namespace URIs and relationship types to be borrowed as static string references with zero heap allocation.

itoa-Based rId Generation

A new make_rid() helper uses itoa::Buffer for stack-based integer formatting, eliminating 21 format!("rId{}", n) heap allocations across the writer pipeline.

Clone this wiki locally