Skip to content

How I Built a Spreadsheet Calculator in the Browser with WASM

I was building a pricing calculator for a client last month. They wanted something that runs entirely in the browser—no backend, no server. The core requirement was simple: users enter a base price, discount percentage, and tax rate, then get the final calculated price.

Simple enough, right?

I started with pure JavaScript. Wrote a quick function that multiplies base price by (1 - discount) and adds tax. It worked. But then the client said, “Can we add tiered pricing? And volume discounts? And what if users want to write their own formulas?”

That’s when things got messy.

The JavaScript Formula Problem

Writing formula parsers from scratch is a rabbit hole. I tried a few approaches:

  1. Eval-based solutions - Dangerous, no Excel compatibility
  2. Custom parser libraries - Limited function support, buggy edge cases
  3. Hyperformula - Good but heavy, lots of dependencies

The real issue was Excel compatibility. The client expected =A1*(1-A2) to work exactly like Excel. They wanted VLOOKUP, IF statements, and later—dynamic arrays like FILTER and XLOOKUP.

That’s when I found Formualizer.

Why WASM Changes Everything

WebAssembly gives you near-native performance in the browser. Formualizer is a Rust-based spreadsheet engine compiled to WASM. It supports 320+ Excel functions out of the box, runs entirely client-side, and has zero backend dependencies.

┌─────────────────────────────────────────────────────────┐
│ Your Web App │
│ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ React/Vue │───▶│ Formualizer (WASM) │ │
│ │ Components │ │ - 320+ Excel functions│ │
│ │ │◀───│ - Dynamic arrays │ │
│ └─────────────────┘ │ - Custom functions │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

No server calls. No API latency. Everything runs in the user’s browser.

Getting Started

Installation is straightforward:

Terminal window
npm install formualizer

The package includes pre-compiled WASM binaries, so no build step required.

Here’s the basic setup:

import init, { Workbook } from 'formualizer';
// Initialize WASM module (call once at app startup)
await init();
// Create workbook and add a sheet
const wb = new Workbook();
wb.addSheet('Calculator');
// Set values - row and column are 1-indexed
wb.setValue('Calculator', 1, 1, 100); // A1 = 100
wb.setValue('Calculator', 2, 1, 0.15); // A2 = 0.15 (15% discount)
// Set a formula
wb.setFormula('Calculator', 1, 2, '=A1*(1-A2)'); // B1 = A1 * (1 - A2)
// Evaluate and get the result
const result = await wb.evaluateCell('Calculator', 1, 2);
console.log(result); // 85

One gotcha: the WASM module needs initialization. I forgot this initially and got cryptic errors. Put await init() in your app’s startup code—once, not every calculation.

Building the Pricing Calculator

Let me show you what I actually built for the client. This is a simplified version:

import init, { Workbook } from 'formualizer';
async function buildPricingCalculator() {
await init();
const wb = new Workbook();
wb.addSheet('Pricing');
// Input cells
wb.setValue('Pricing', 1, 1, 'Base Price');
wb.setValue('Pricing', 1, 2, 299.99);
wb.setValue('Pricing', 2, 1, 'Discount %');
wb.setValue('Pricing', 2, 2, 0.10); // 10%
wb.setValue('Pricing', 3, 1, 'Tax Rate');
wb.setValue('Pricing', 3, 2, 0.08); // 8%
// Calculated cells with formulas
wb.setValue('Pricing', 4, 1, 'Discounted Price');
wb.setFormula('Pricing', 4, 2, '=B2*(1-B3)');
wb.setValue('Pricing', 5, 1, 'Tax Amount');
wb.setFormula('Pricing', 5, 2, '=B4*B5'); // Wait, this is wrong!
wb.setValue('Pricing', 6, 1, 'Total');
wb.setFormula('Pricing', 6, 2, '=B4+B5');
const total = await wb.evaluateCell('Pricing', 6, 2);
return total;
}

I made a mistake in the code above—can you spot it? The tax formula references B5 before B5 is defined. Formualizer handles dependency resolution, but the logic should be:

// Correct tax formula
wb.setFormula('Pricing', 5, 2, '=B4*B3'); // discounted price * tax rate

This is the kind of error that would have been a nightmare to debug without Excel-like formula support. The nice part is you see exactly what’s wrong because formulas look exactly like Excel.

Dynamic Arrays: The Game Changer

The client later asked for a product lookup feature. They had a product table and wanted users to filter by stock status and look up prices.

This is where dynamic arrays shine. Formualizer supports FILTER, UNIQUE, SORT, XLOOKUP, and SEQUENCE:

async function productLookupExample() {
await init();
const wb = new Workbook();
wb.addSheet('Products');
// Product data
// A B C
// 1 Product Price Stock
// 2 Widget A 10 50
// 3 Widget B 15 0
// 4 Widget C 20 100
wb.setValue('Products', 1, 1, 'Product');
wb.setValue('Products', 1, 2, 'Price');
wb.setValue('Products', 1, 3, 'Stock');
wb.setValue('Products', 2, 1, 'Widget A');
wb.setValue('Products', 2, 2, 10);
wb.setValue('Products', 2, 3, 50);
wb.setValue('Products', 3, 1, 'Widget B');
wb.setValue('Products', 3, 2, 15);
wb.setValue('Products', 3, 3, 0);
wb.setValue('Products', 4, 1, 'Widget C');
wb.setValue('Products', 4, 2, 20);
wb.setValue('Products', 4, 3, 100);
// FILTER: Get all products with stock > 0
wb.setFormula('Products', 6, 1, '=FILTER(A2:C4, C2:C4>0, "None in stock")');
// XLOOKUP: Find price for "Widget C"
wb.setFormula('Products', 7, 1, '=XLOOKUP("Widget C", A2:A4, B2:B4)');
const inStock = await wb.evaluateCell('Products', 6, 1);
const widgetCPrice = await wb.evaluateCell('Products', 7, 1);
console.log(inStock); // Array of in-stock products
console.log(widgetCPrice); // 20
}

The FILTER function spills results into multiple cells. Formualizer handles this correctly—it returns an array when the result spans cells.

Custom Functions for Business Logic

Sometimes standard Excel functions aren’t enough. Formualizer lets you register custom JavaScript functions:

async function withCustomFunction() {
await init();
const wb = new Workbook();
wb.addSheet('Custom');
// Register a custom DISCOUNT function
wb.registerFunction('DISCOUNT', (price: number, tier: string) => {
const rates: Record<string, number> = {
'basic': 0.05,
'premium': 0.15,
'enterprise': 0.25
};
return price * (1 - (rates[tier] || 0));
});
wb.setValue('Custom', 1, 1, 1000);
wb.setValue('Custom', 2, 1, 'premium');
wb.setFormula('Custom', 3, 1, '=DISCOUNT(A1, A2)');
const result = await wb.evaluateCell('Custom', 3, 1);
console.log(result); // 850 (1000 * 0.85)
}

This is powerful. You can expose domain-specific business logic to your spreadsheet users without them knowing it’s JavaScript.

React Integration

I wrapped everything in a React component for the client. Here’s the pattern I used:

import { useEffect, useState, useCallback } from 'react';
import init, { Workbook } from 'formualizer';
export function PricingCalculator() {
const [wb, setWb] = useState<Workbook | null>(null);
const [result, setResult] = useState<number>(0);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function initWasm() {
await init();
const workbook = new Workbook();
workbook.addSheet('Calc');
// Set up initial formulas
workbook.setFormula('Calc', 3, 1, '=A1*(1-A2)');
setWb(workbook);
setLoading(false);
}
initWasm();
}, []);
const calculate = useCallback(async (base: number, discount: number) => {
if (!wb) return;
wb.setValue('Calc', 1, 1, base);
wb.setValue('Calc', 2, 1, discount);
const total = await wb.evaluateCell('Calc', 3, 1);
setResult(total);
}, [wb]);
if (loading) return <div>Loading...</div>;
return (
<div>
<input
type="number"
onChange={(e) => calculate(Number(e.target.value), 0.1)}
/>
<p>Final Price: ${result.toFixed(2)}</p>
</div>
);
}

Key lesson: initialize WASM once in useEffect, not on every render. Also, memoize calculation functions with useCallback to avoid re-creating the workbook reference.

Performance Considerations

For the client’s full product catalog (5000+ rows), I had to optimize:

  1. Batch updates - Set multiple values before evaluating
  2. Lazy evaluation - Only recalculate affected cells
  3. Web Workers - Move computation off the main thread for huge datasets
// Instead of evaluating after each change
wb.setValue('Sheet', 1, 1, 100);
const r1 = await wb.evaluateCell('Sheet', 1, 2); // Slow!
// Batch and evaluate once
wb.setValue('Sheet', 1, 1, 100);
wb.setValue('Sheet', 2, 1, 200);
wb.setValue('Sheet', 3, 1, 300);
// ... more changes
const r3 = await wb.evaluateCell('Sheet', 3, 2); // Much faster

Formualizer’s dependency graph means it only recalculates what’s needed. But minimizing evaluation calls still helps.

What I Learned

  1. WASM initialization is async - Don’t forget await init()
  2. Formulas are 1-indexed - A1 is row 1, column 1, not 0, 0
  3. Dependency order matters - Define cells before formulas that reference them
  4. Dynamic arrays return arrays - Handle accordingly in your UI
  5. Custom functions bridge business logic - Expose domain functions without exposing code

The client got exactly what they wanted: a fast, offline-capable pricing calculator with Excel-compatible formulas. They can even write their own formulas for complex pricing models.

If you’re building anything that needs spreadsheet functionality in the browser, Formualizer + WASM is the path I’d take again.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments