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:
- Eval-based solutions - Dangerous, no Excel compatibility
- Custom parser libraries - Limited function support, buggy edge cases
- 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:
npm install formualizerThe 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 sheetconst wb = new Workbook();wb.addSheet('Calculator');
// Set values - row and column are 1-indexedwb.setValue('Calculator', 1, 1, 100); // A1 = 100wb.setValue('Calculator', 2, 1, 0.15); // A2 = 0.15 (15% discount)
// Set a formulawb.setFormula('Calculator', 1, 2, '=A1*(1-A2)'); // B1 = A1 * (1 - A2)
// Evaluate and get the resultconst result = await wb.evaluateCell('Calculator', 1, 2);console.log(result); // 85One 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 formulawb.setFormula('Pricing', 5, 2, '=B4*B3'); // discounted price * tax rateThis 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:
- Batch updates - Set multiple values before evaluating
- Lazy evaluation - Only recalculate affected cells
- Web Workers - Move computation off the main thread for huge datasets
// Instead of evaluating after each changewb.setValue('Sheet', 1, 1, 100);const r1 = await wb.evaluateCell('Sheet', 1, 2); // Slow!
// Batch and evaluate oncewb.setValue('Sheet', 1, 1, 100);wb.setValue('Sheet', 2, 1, 200);wb.setValue('Sheet', 3, 1, 300);// ... more changesconst r3 = await wb.evaluateCell('Sheet', 3, 2); // Much fasterFormualizer’s dependency graph means it only recalculates what’s needed. But minimizing evaluation calls still helps.
What I Learned
- WASM initialization is async - Don’t forget
await init() - Formulas are 1-indexed - A1 is row 1, column 1, not 0, 0
- Dependency order matters - Define cells before formulas that reference them
- Dynamic arrays return arrays - Handle accordingly in your UI
- 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