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.0).

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.

Clone this wiki locally