Titan Engine
ZERO-COPY WASM ENGINE
Star on GitHub ★

Excel-scale recalculation in WebAssembly.

Titan compiles formulas into bytecode and stores cells in contiguous 32×32 Struct-of-Arrays chunks inside WASM linear memory. No JavaScript object churn, no GC pauses, and sub-millisecond reactive updates.

npm install titan-engine
Memory Layout
32×32 SoA Chunks
Execution
Pratt AST → Stack VM
Graph Solver
Bipartite Kahn DAG
Standard Library
43 Native Functions
LIVE WORKBENCH 5,000 rows × 13 cols · 30,000 active formulas
Click any cell or edit the formula bar · Cmd/Ctrl+Z to undo
200×
Faster VLOOKUP (10k rows)
58 ms vs. 11,694 ms in HF
122×
Faster SUMIF (100M cell scan)
300 ms vs. 36,845 ms in HF
0.17 ms
20k-Formula Row Insertion
In-chunk SoA memmove
7.9×
Faster Formula Compilation
30,000 unique formulas in 71 ms

Why JavaScript spreadsheets stall—and how Titan avoids it.

Most web spreadsheets represent every cell as a heap-allocated JavaScript object and serialize results across boundaries. Titan keeps the entire calculation lifecycle inside flat WebAssembly arrays.

01 · Data Layout 32 × 32 SoA

Struct-of-Arrays Chunk Memory

Sheets are partitioned into sparse 32×32 chunks (1,024 cells). Each chunk separates cell tags from 64-bit payloads in contiguous arrays so sequential column scans stay in L1 cache and JavaScript reads via zero-copy Float64Array views.

Chunk::types Box<[u8; 1024]> (1 KB)
Chunk::payloads Box<[u64; 1024]> (8 KB)
JS Viewport Read Pointer offset · 0 allocations
02 · Execution Pipeline BYTECODE VM

Pratt AST to Stack Bytecode with Lazy Ranges

Formulas compile once into flat OpCode vectors. Ranges push lightweight 5-integer RangeRef descriptors onto the VM stack rather than allocating intermediate arrays, and IF / IFERROR compile to native jump instructions (JmpIfFalse, TryCatch, EndTry).

// Input: =IFERROR(IF(A1>0, VLOOKUP("ID-9", A2:B5000, 2, FALSE), 0), -1)
00: TryCatch(jmp -> 11)
06: PushNum(2.0), PushBool(false)
01: PushRef(0, 0, 0) > PushNum(0)
07: Call(VLOOKUP, argc=4)
02: JmpIfFalse(jmp -> 09)
08: Jmp(jmp -> 10)
03: PushStr(pool_id=14)
09: PushNum(0.0)
04: PushRange(0, 1, 0, 4999, 1)
10: EndTry, Jmp(end)
03 · Bipartite Range DAG & Ref-Counted StringPool KAHN TOPO-SORT

30,000 Graph Edges Instead of 200,000,000

When 20,000 cells in A1:B10000 update alongside 10,000 VLOOKUP or SUMIF formulas, naive engines build 20,000 × 10,000 = 200,000,000 cell-to-cell dependency edges. Titan condenses ranges into intermediate DAG nodes during Kahn's topological sort—visiting each range's dependents exactly once when its in-degree reaches zero.

Naive Cell-to-Cell DAG 200,000,000 edges (1.6 GB)
Titan Bipartite RangeRef DAG 30,000 edges (< 1 MB)
Circular Reference Handling Isolated #CYCLE! per node

Head-to-Head vs. HyperFormula

Measured in Chrome on macOS · Run the suite yourself in your own browser tab.

Open Live Benchmark Runner →
Scenario Scale HyperFormula Titan (WASM) Speedup
VLOOKUP Scan 10,000 lookups 11,694.8 ms 58.6 ms 199.6× faster
Conditional SUMIF 10,000 formulas 36,845.9 ms 300.7 ms 122.5× faster
Multi-Criteria SUMIFS 5,000 formulas 14,847.3 ms 115.0 ms 129.1× faster
Formula Compilation 30,000 unique 561.3 ms 71.4 ms 7.9× faster
Matrix Broadcasting 100,000 evals 34.2 ms 9.2 ms 3.7× faster
Deep Topo Cascade 20,000 chain 27.4 ms 8.5 ms 3.2× faster
Raw Cell Allocation 100,000 cells 175.1 ms 54.3 ms 3.2× faster

Drop-In Ergonomics

Polymorphic A1 or {row, col} addressing with reactive delta subscriptions.

quickstart.js ES Module
import { initTitan } from 'titan-engine';

const Titan = await initTitan();
const engine = new Titan();
const sheet = engine.addSheet("Revenue");

// Subscribe to cascading cell changes
engine.onCellsChanged((deltas) => {
  for (const { a1, cell } of deltas) {
    console.log(`${a1} -> ${cell.display}`);
  }
});

// Batch-insert a 2D range and evaluate DAG once
sheet.setRange("A1", [
  ["Q1",  12500],
  ["Q2",  18400],
  ["Sum", "=SUM(B1:B2)"],
  ["Tax", "=IF(B3>25000, B3*0.2, 0)"]
]);