Every number on this page comes from a validated local protocol artifact captured on a clean tree; nothing is published from an unvalidated or protocol-mismatched artifact. Every expected cell carries either a validated timing or its recorded failure - a run that did not complete is shown as a failure, never converted into a timing.
Render benchmark: Sheetwrite vs Handsontable
Both engines drive identical scripted interactions in a controlled browser. Pick a workbook size and a metric:
scroll-down.top-left22.8× fasterscroll-down.middle3.9× fasterscroll-smooth.same-window51.6× fasterscroll-right.top-left96.7× fasteredit-open.top-left2.3× fasteredit-open.middle1.9× fasteredit-open.bottom-right3.5× fasteredit-commit.middle7.2× fasteraltering.insert-5-rows-top21.9× fasteraltering.remove-5-rows-top22.4× fasterarrow-down.top-left2.6× fasterarrow-right.middle2.5× fasterformatted-paint.top-left4.7× fastermerge-heavy.paint15.1× fasterscroll-down.top-left1.5× leanerscroll-down.middle1.6× leanerscroll-smooth.same-window1.5× leanerscroll-right.top-left1.3× leaneredit-open.top-left1.7× leaneredit-open.middle2.5× leaneredit-open.bottom-right2.2× leaneredit-commit.middle1.9× leaneraltering.insert-5-rows-top2.3× leaneraltering.remove-5-rows-top2.0× leanerarrow-down.top-left2.1× leanerarrow-right.middle2.0× leanerformatted-paint.top-left3.1× leanermerge-heavy.paint2.1× leanerscroll-down.top-left26.7× fasterscroll-down.middle6.0× fasterscroll-smooth.same-window60.7× fasterscroll-right.top-left111.3× fasteredit-open.top-left2.4× fasteredit-open.middle2.0× fasteredit-open.bottom-right3.8× fasteredit-commit.middle10.7× fasteraltering.insert-5-rows-top23.7× fasteraltering.remove-5-rows-top29.7× fasterarrow-down.top-left2.5× fasterarrow-right.middle2.7× fasterformatted-paint.top-left4.7× fastermerge-heavy.paint13.0× fasterscroll-down.top-left1.9× leanerscroll-down.middle1.8× leanerscroll-smooth.same-window1.7× leanerscroll-right.top-left1.1× leaneredit-open.top-left2.1× leaneredit-open.middle2.0× leaneredit-open.bottom-right2.2× leaneredit-commit.middle2.0× leaneraltering.insert-5-rows-top2.6× leaneraltering.remove-5-rows-top3.3× leanerarrow-down.top-left3.0× leanerarrow-right.middle2.6× leanerformatted-paint.top-left3.4× leanermerge-heavy.paint3.4× leanerscroll-down.top-left67.4× fasterscroll-down.middle26.3× fasterscroll-smooth.same-window173.2× fasterscroll-right.top-left201.2× fasteredit-open.top-left2.3× fasteredit-open.middle2.7× fasteredit-open.bottom-right4.8× fasteredit-commit.middle45.4× fasteraltering.insert-5-rows-top44.8× fasteraltering.remove-5-rows-top42.0× fasterarrow-down.top-left2.5× fasterarrow-right.middle3.2× fasterformatted-paint.top-left4.3× fastermerge-heavy.paint22.6× fasterscroll-down.top-left2.2× leanerscroll-down.middle2.5× leanerscroll-smooth.same-window2.1× leanerscroll-right.top-left1.7× leaneredit-open.top-left2.8× leaneredit-open.middle2.9× leaneredit-open.bottom-right2.6× leaneredit-commit.middle2.8× leaneraltering.insert-5-rows-top3.0× leaneraltering.remove-5-rows-top3.4× leanerarrow-down.top-left4.4× leanerarrow-right.middle3.7× leanerformatted-paint.top-left3.5× leanermerge-heavy.paint3.4× leanerscroll-down.top-left538.0× fasterscroll-down.middle296.6× fasterscroll-smooth.same-window1522.2× fasterscroll-right.top-left1352.1× fasteredit-open.top-left2.5× fasteredit-open.middle3.4× fasteredit-open.bottom-right5.3× fasteredit-commit.middle475.3× fasteraltering.insert-5-rows-toponly Sheetwrite completedaltering.remove-5-rows-toponly Sheetwrite completedarrow-down.top-left2.9× fasterarrow-right.middle4.1× fasterformatted-paint.top-left4.2× fastermerge-heavy.paint130.3× fasterscroll-down.top-left2.4× leanerscroll-down.middle3.8× leanerscroll-smooth.same-window3.6× leanerscroll-right.top-left3.6× leaneredit-open.top-left2.4× leaneredit-open.middle2.3× leaneredit-open.bottom-right2.2× leaneredit-commit.middle2.1× leaneraltering.insert-5-rows-toponly Sheetwrite completedaltering.remove-5-rows-toponly Sheetwrite completedarrow-down.top-left5.9× leanerarrow-right.middle6.3× leanerformatted-paint.top-left6.0× leanermerge-heavy.paint5.2× leanerMethodology - what each scenario does
Live grid in controlled headless Chromium. Ten counterbalanced rounds; all fourteen scenarios run warm per mount, and the fixture is rebuilt after any failure so crashes cannot leak state. Every scenario must prove its effect (scroll really moved, editor really opened, rows really changed) or it fails.
Bright = median round, faded = p95 round. Did not complete = recorded crash, timeout, or failed checkpoint - never a timing.
scroll-down.top-left- From the origin, jump-scroll 50 px down: a fresh row band enters the viewport and must paint.
scroll-down.middle- The same 50 px jump starting from the vertical middle of the scroll range.
scroll-smooth.same-window- Scroll 1 px without changing the visible row window: pure repaint cost, zero new data.
scroll-right.top-left- Jump-scroll 50 px right: a fresh column band paints.
edit-open.top-left- Select a cell near the origin and open its editor.
edit-open.middle- Open the editor on the center cell of the workbook.
edit-open.bottom-right- Open the editor on the last row and column - the far end of every index.
edit-commit.middle- Commit a typed value into the center cell and paint the result.
altering.insert-5-rows-top- Insert five rows at the top: every following row reindexes.
altering.remove-5-rows-top- Remove those five rows again - the inverse reindex.
arrow-down.top-left- Move the selection one cell down with the arrow key, including the selection overlay repaint.
arrow-right.middle- Arrow-key selection move at the workbook center.
formatted-paint.top-left- Repaint a viewport dense with per-cell formatting.
merge-heavy.paint- Repaint a viewport dense with merged ranges; Sheetwrite additionally proves it builds exactly one merge revision index.
Reproduce and validate with:
bun run --filter @sheetwrite/bench bench:render:preparebun run --filter @sheetwrite/bench bench:render:scaleData engine benchmark
ingest1556.9× fasterwindowRead13.6× fasteredit105.4× fastersort1762.9× fasterfilter4708.7× fasteraggregate57.7× fasteringest1709.6× fasterwindowRead20.2× fasteredit662.0× fastersort9977.5× fasterfilter20304.6× fasteraggregate72.0× fasterSheetwrite alone at scale — Handsontable cannot complete these sizes headlessly:
ingestwindowReadeditsortfilteraggregateMethodology - what each operation does
Identical columnar datasets, per-operation warmup and iteration plans. Fresh instance per ingest; window reads rotate offsets to defeat caches; sort and filter reset between runs; Handsontable edits run with rendering suspended so only its data path is timed.
Memory = JS-heap delta around one ingest in an isolated subprocess, plus Sheetwrite's WASM linear-memory delta.
ingest- Load the full dataset into a fresh engine instance.
windowRead- Read a 50x5 cell window at a rotating offset that sweeps the whole sheet.
edit- 1,000 single-cell edits.
sort- Sort by the numeric amount column.
filter- Substring filter over the city column.
aggregate- Numeric aggregation over the amount column.
Reproduce with:
bun run --filter @sheetwrite/bench bench:dataFormula engine benchmark
criteria-range-editcross-sheet-range-editcyclesdiamond-editdistinct-range-editerror-propagationindependent-first-recomputeindependent-parse-loadlinear-chainlookup-range-editremoved-sheet-refshared-range-edittopology-remove-addwide-fan-out-editscalar-edit-affectsMethodology - what each workload does
Each workload builds a fresh WASM cell-store of the named dependency shape and times the recalculation from one action - usually a single edit. Sizes are formula-cell counts; bright = median, faded = p95.
linear-chain- A chain A1 -> A2 -> ... -> AN; editing the head recomputes the full depth.
wide-fan-out-edit- One scalar feeds N dependent formulas; edit the scalar.
diamond-edit- Fan-out that reconverges (diamond graph); edit the apex.
shared-range-edit- N formulas aggregate one shared range; edit one cell inside it.
distinct-range-edit- Each formula owns its own range; one edit recomputes only its owner.
cross-sheet-range-edit- Summary-sheet formulas range over another sheet; edit the source.
scalar-edit-affects-N- One scalar edit invalidating exactly N dependents; N=0 touches an unrelated cell, so it prices pure dependency lookup.
topology-remove-add- Remove and re-add rows so the dependency graph itself changes shape.
cycles- Introduce a reference cycle; detection and cycle-error propagation.
removed-sheet-ref- Formulas referencing a deleted sheet must all degrade to reference errors.
error-propagation- An error value (=1/0) flows through every dependent.
criteria-range-edit- Criteria-style aggregation (SUMIF shape) over 100k cells; edit inside the criteria range.
lookup-range-edit- Lookup-shape formulas over 100k cells; edit inside the looked-up range.
Reproduce with:
bun run --filter @sheetwrite/bench bench:formulaDelivery size
| Package | Tarball | Unpacked |
|---|---|---|
@sheetwrite/core | 372.4 KiB | 1958.3 KiB |
@sheetwrite/react | 7.0 KiB | 22.0 KiB |
@sheetwrite/svelte | 5.2 KiB | 14.6 KiB |
@sheetwrite/vue | 8.6 KiB | 28.5 KiB |
@sheetwrite/wasm | 180.5 KiB | 490.4 KiB |
@sheetwrite/xlsx | 75.0 KiB | 366.1 KiB |
A minimal Vite app that renders a grid ships 85.6 KiB of gzipped JavaScript.
Reproduce with:
bun run size:report