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.
Release retained engine memory
WebAssembly linear memory grows but does not shrink after cells or stores are cleared. When your application is idle after a large import, clear, or workbook disposal, call releaseEngineMemory explicitly. Sheetwrite never schedules a release automatically.
The call waits for synchronous transactions and active datasource page loads, then moves all live stores together into a fresh instance of the selected engine. Cell sources, styles, cached formula results (including volatile results), resident datasource pages, unsaved dirty edits, and compact undo/redo history survive. Unloaded clean pages remain unloaded and can be fetched again from the datasource. Worker renderers rebuild their frames without document-change events.
The main-thread pause scales with resident cells and compact history, so do not invoke it on an interaction-critical path. Migration temporarily needs both instances and packed transfer buffers; it can increase peak memory before garbage collection reclaims the retired instance. If preparation fails, every live store stays on the original instance. Concurrent calls share the same release.
report.wasmBytesBefore and report.wasmBytesAfter are committed linear-memory bytes, not live-cell bytes or process memory. report.pauseMs measures migration, excluding the idle wait. A release is not guaranteed to reduce committed bytes: surviving data, compact history, and migration scratch can still require substantial memory. Actual process-memory reclamation depends on garbage collection.
Capture
bench:release captures render-scale, the performance showcase's paged storage at bench/results/paged-results.json, the interaction evidence at bench/results/interaction-results.json, data, core paths, formula, formula engines, matched engines, and XLSX, then re-records the render regression baseline last. Its smoke rehearsal writes only to bench/results/smoke/.
The interaction capture (bench:interaction, schema interaction-current-v2) measures the current build, not a comparison: packed inverse-index lookup latency in nanoseconds, inverse-index backing bytes, retained bytes after 100 distant edits, and the cold-route long task owned by Sheetwrite. It records the raw samples, the summary derived from them, the runner, and the method, and it holds those four metrics to absolute release ceilings. Version 1 recorded a one-off before/after pair whose baseline build cannot be re-measured at a later release, so no old sample is carried forward into the current capture.
Matched local regression check
Timing comparisons run deliberately on a controlled local machine, not as a required CI job. Capture ten fresh matched rounds, retain every raw sample, and compare the fresh artifact with the committed baseline. Any unapproved slowdown fails the local command.
bun run --filter @sheetwrite/bench bench:render:preparecd benchbun run src/render-driver.ts --rounds 10 --output results/render-fresh.json --markdown-output results/render-fresh.mdbun run src/check.ts --baseline results/render-baseline.json --fresh results/render-fresh.json --power-mode balanced --concurrency 1A result is a regression decision only when that final baseline check passes on the declared power mode and concurrency. bench:verify remains a smoke and safety-ceiling check.
The committed baseline at bench/results/render-baseline.json pins the harness source hashes, the declared sampling, and the runner (OS, CPU, Bun, Node, Chromium, power mode, concurrency). It is therefore machine- and harness-pinned: it must be re-recorded whenever a harness file, the sampling flags, the protocol version, or the runner changes, and the release capture does that as its last step by re-recording ten controlled rounds into bench/results/render-baseline-raw.json and promoting that artifact over the committed baseline. Until the re-recorded baseline is committed, bench:check fails closed on the fingerprint mismatch instead of comparing unrelated measurements.
Render benchmark
Both engines drive identical scripted interactions in a controlled browser. Pick a workbook size and a metric:
scroll-down.top-left14.6× fasterscroll-down.middle4.0× fasterscroll-smooth.same-window21.4× fasterscroll-right.top-left43.3× fasteredit-open.top-left2.1× fasteredit-open.middle2.0× fasteredit-open.bottom-right3.5× fasteredit-commit.middle6.6× fasteraltering.insert-5-rows-top6.5× fasteraltering.remove-5-rows-top6.5× fasterarrow-down.top-left2.6× fasterarrow-right.middle2.3× fasterformatted-paint.top-left9.1× fastermerge-heavy.paint1112.4× fasterscroll-down.top-left1.5× leanerscroll-down.middle1.5× leanerscroll-smooth.same-window1.5× leanerscroll-right.top-left1.2× leaneredit-open.top-left1.7× leaneredit-open.middle1.5× leaneredit-open.bottom-right1.9× leaneredit-commit.middle1.9× leaneraltering.insert-5-rows-top1.9× leaneraltering.remove-5-rows-top2.0× leanerarrow-down.top-left2.1× leanerarrow-right.middle1.8× leanerformatted-paint.top-left2.5× leanermerge-heavy.paint1.4× leanerscroll-down.top-left14.2× fasterscroll-down.middle3.5× fasterscroll-smooth.same-window20.9× fasterscroll-right.top-left42.6× fasteredit-open.top-left2.3× fasteredit-open.middle1.8× fasteredit-open.bottom-right3.3× fasteredit-commit.middle6.4× fasteraltering.insert-5-rows-top6.5× fasteraltering.remove-5-rows-top6.4× fasterarrow-down.top-left2.5× fasterarrow-right.middle2.4× fasterformatted-paint.top-left9.3× fastermerge-heavy.paint1049.8× fasterscroll-down.top-left1.5× leanerscroll-down.middle1.5× leanerscroll-smooth.same-window1.5× leanerscroll-right.top-left1.2× leaneredit-open.top-left1.5× leaneredit-open.middle2.2× leaneredit-open.bottom-right2.1× leaneredit-commit.middle1.4× leaneraltering.insert-5-rows-top3.3× leaneraltering.remove-5-rows-top4.0× leanerarrow-down.top-left3.3× leanerarrow-right.middle3.7× leanerformatted-paint.top-left3.6× leanermerge-heavy.paint2.3× leanerscroll-down.top-left13.8× fasterscroll-down.middle3.9× fasterscroll-smooth.same-window20.7× fasterscroll-right.top-left41.0× fasteredit-open.top-left2.2× fasteredit-open.middle1.9× fasteredit-open.bottom-right3.4× fasteredit-commit.middle7.0× fasteraltering.insert-5-rows-top8.2× fasteraltering.remove-5-rows-top8.9× fasterarrow-down.top-left2.4× fasterarrow-right.middle2.3× fasterformatted-paint.top-left8.7× fastermerge-heavy.paint931.0× fasterscroll-down.top-left2.2× leanerscroll-down.middle1.8× leanerscroll-smooth.same-window1.7× leanerscroll-right.top-left1.4× leaneredit-open.top-left2.2× leaneredit-open.middle2.5× leaneredit-open.bottom-right2.4× leaneredit-commit.middle2.5× leaneraltering.insert-5-rows-top7.0× leaneraltering.remove-5-rows-top8.9× leanerarrow-down.top-left5.9× leanerarrow-right.middle5.7× leanerformatted-paint.top-left4.2× leanermerge-heavy.paint3.5× leanerscroll-down.top-left12.7× fasterscroll-down.middle4.0× fasterscroll-smooth.same-window20.7× fasterscroll-right.top-left42.9× fasteredit-open.top-left2.4× fasteredit-open.middle1.6× fasteredit-open.bottom-right3.1× fasteredit-commit.middle5.8× fasteraltering.insert-5-rows-top8.2× fasteraltering.remove-5-rows-top7.2× fasterarrow-down.top-left2.3× fasterarrow-right.middle2.4× fasterformatted-paint.top-left7.1× fastermerge-heavy.paint949.8× fasterscroll-down.top-left2.3× leanerscroll-down.middle2.8× leanerscroll-smooth.same-window2.1× leanerscroll-right.top-left2.2× leaneredit-open.top-left2.7× leaneredit-open.middle2.7× leaneredit-open.bottom-right2.6× leaneredit-commit.middle2.5× leaneraltering.insert-5-rows-top11.3× leaneraltering.remove-5-rows-top6.9× leanerarrow-down.top-left7.2× leanerarrow-right.middle7.0× leanerformatted-paint.top-left7.0× leanermerge-heavy.paint4.6× 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.
Each measured sample repeats one logical action until it covers at least 100 ms of measured time, so timer resolution and scheduler jitter cannot decide a sample. Every raw sample is retained in the artifact.
Bright = median round, faded = p95 round. Did not complete = recorded crash, timeout, or failed checkpoint - never a timing. The failure table names the stage and the recorded error behind every missing cell.
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:scaleRuntime resource ownership
The performance showcase at /showcases/performance#resource-ownership renders the public RuntimeResourceSnapshot protocol directly. It keeps exclusive-owner logical bytes, allocated capacity, WASM committed pages, and independent browser runtime observations in separate buckets; committed pages are never summed into live payload. Bulk-edit before/settled deltas use diffRuntimeResourcePhases and omit unchanged owners.
The UI and this reference share RUNTIME_RESOURCE_SCHEMA_VERSION. Detailed peak phases remain in the validated benchmark artifact rather than being presented as measurements from the visitor's browser.
String collection reclaims obsolete pool text and densely remaps surviving IDs. Automatic collection runs only at committed edit transaction barriers, never during painting or page loading, when dead arena/span storage exceeds 256 KiB and the surviving arena/span storage. Growth-based backoff avoids repeatedly scanning unchanged live data. Bulk clears and sheet removal request a new threshold check at the next commit.
compactStringStorage() explicitly collects unreachable text and trims growth slack outside an active mutation/page load. Raw IDs are valid only within stringPoolEpoch(); retain the epoch with projections and invalidate caches on a change, as described in the worker rendering guide. History snapshots own captured text. Persist resolved values, not pool IDs. Reclaimed Rust ownership does not imply that WebAssembly committed linear memory shrinks.
Reproduce and validate the full owner/operation matrix with:
bun run --filter @sheetwrite/bench bench:resourceData engine benchmark
ingest440.5× fasterwindowRead17.8× fasteredit32.8× fastersort728.5× fasterfilter2258.1× fasteraggregate17.1× fasteringest478.6× fasterwindowRead16.2× fasteredit150.1× fastersort11379.1× fasterfilter25557.3× fasteraggregate20.3× 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:dataCore write path
- Grid.applyTransaction of a 1,000,000-cell setBlock
- 488.7 ms
- fromCsv import of 200,000 rows by 10 columns
- 234.6 ms
- undo of a 1,000,000-cell clearRange
- 1,609.1 ms
- cold initSheetwrite() through first store in a fresh Node process (7 processes)
- 4.24 ms
| Scenario | One timed iteration | Median | p95 |
|---|---|---|---|
row-bridge-insert | one addRows projection of 10,000 rows into a 1,000,000-row bridge | 5.95 ms | 6.30 ms |
command-state-select | one 4,096-cell range selection on a framework-style grid | 6.71 ms | 8.84 ms |
with-command-state-handler | Variant of command-state-select, measured in the same run | 6.71 ms | 8.84 ms |
without-command-state-handler | Variant of command-state-select, measured in the same run | 5.07 ms | 5.75 ms |
setblock-admission | one Grid.applyTransaction of a 1,000,000-cell setBlock | 488.7 ms | 503.5 ms |
setblock-detailed-capture | one Grid.applyTransaction of a 1,000,000-cell setBlock observed through a change listener with detailed capture | 564.4 ms | 568.2 ms |
sync-offline-queue | one grid commit admitted into an offline sync queue with no listeners | 0.67 ms | 0.80 ms |
rebase-large | one rebase of a 100,000-cell block across 50 single-row inserts | 1.54 ms | 1.54 ms |
validation-sparse | one 100,000-cell sparse setRange with 100 rules outside the range | 168.9 ms | 169.4 ms |
csv-import | one fromCsv import of 200,000 rows by 10 columns | 234.6 ms | 235.3 ms |
undo-large-clear | one undo of a 1,000,000-cell clearRange | 1,609.1 ms | 1,621.4 ms |
restore-near-ceiling | one restoreBlock with 60000044 decoded bytes | 1,060.1 ms | 1,069.8 ms |
paged-evicted-revisit | one revisit of an evicted paged band: scroll back plus a fixed async settle floor (wall time is not the accept metric) | 5.86 ms | 6.97 ms |
node-cold-init | one cold initSheetwrite() through first store in a fresh Node process (7 processes) | 4.24 ms | 4.58 ms |
All measurements
Scale
| Parameter | Value |
|---|---|
bridgeRows | 1,000,000 |
bridgeInsertRows | 10,000 |
selectionRows | 64 |
selectionColumns | 64 |
blockRows | 1,000 |
blockColumns | 1,000 |
offlineCommits | 5,000 |
rebaseBlockRows | 1,000 |
rebaseBlockColumns | 100 |
rebaseInsertOperations | 50 |
sparseCells | 100,000 |
validationRules | 100 |
csvRows | 200,000 |
csvColumns | 10 |
clearRows | 1,000 |
clearColumns | 1,000 |
pagedRows | 200,000 |
Samples
| Scenario | Samples |
|---|---|
row-bridge-insert | 2 |
command-state-select | 10 |
with-command-state-handler | 10 |
without-command-state-handler | 10 |
setblock-admission | 3 |
setblock-detailed-capture | 3 |
sync-offline-queue | 5000 |
rebase-large | 3 |
validation-sparse | 3 |
csv-import | 2 |
undo-large-clear | 3 |
restore-near-ceiling | 3 |
paged-evicted-revisit | 3 |
node-cold-init | 6 |
Counters
Non-time evidence recorded by the same runs.
| Scenario | Counter | Value |
|---|---|---|
row-bridge-insert | bridgeRows | 1,000,000 |
row-bridge-insert | insertedRows | 10,000 |
command-state-select | selectedCells | 4,096 |
command-state-select | commandStateEvents | 13 |
command-state-select | withoutHandlerMedianMs | 5.073 |
setblock-admission | blockCells | 1,000,000 |
setblock-admission | transactions | 4 |
setblock-detailed-capture | blockCells | 1,000,000 |
setblock-detailed-capture | transactions | 4 |
setblock-detailed-capture | capturedChanges | 1,000,000 |
setblock-detailed-capture | validatedCapturedChanges | 1,000,000 |
sync-offline-queue | queuedCommits | 5,005 |
sync-offline-queue | admittedOperations | 5,005 |
rebase-large | blockCells | 100,000 |
rebase-large | insertOperations | 50 |
rebase-large | verifiedValues | 100,000 |
validation-sparse | writtenCells | 100,000 |
validation-sparse | logicalRangeCells | 1,000,000 |
validation-sparse | validationRules | 100 |
csv-import | importedRows | 200,000 |
csv-import | importedColumns | 10 |
csv-import | verifiedCells | 2,000,000 |
csv-import | inputChars | 20,888,985 |
undo-large-clear | clearedCells | 1,000,000 |
undo-large-clear | clears | 4 |
undo-large-clear | restoredCells | 1,000,000 |
undo-large-clear | compactRestores | 5 |
undo-large-clear | peakRssBytes | 1,143,246,848 |
restore-near-ceiling | restoredCells | 4,000,000 |
restore-near-ceiling | decodedBytes | 60,000,044 |
restore-near-ceiling | peakRssBytes | 1,509,625,856 |
paged-evicted-revisit | revisitedRows | 3 |
paged-evicted-revisit | areColumnsFullyLoadedCalls | 108 |
paged-evicted-revisit | evictionVisits | 3 |
paged-evicted-revisit | pagedRows | 200,000 |
node-cold-init | largestHeartbeatGapMs | 3.73 |
node-cold-init | minimalHeartbeatTicks | 1 |
node-cold-init | freshProcesses | 7 |
Correctness checkpoints
| Scenario | Checkpoint |
|---|---|
row-bridge-insert | projected delta is one row-structure insert with every created identity present in the bridge in order |
command-state-select | selection covers 4096 cells and the command-state handler observes one event per selection |
setblock-admission | every setBlock is applied and every cell of the block holds the last written value |
setblock-detailed-capture | every setBlock is applied and every cell of the block holds the last written value, and its change capture reports every cell's address, previous value, and new value exactly once |
sync-offline-queue | every commit is applied, the queue holds every commit at server version 0, and capacity stays available |
rebase-large | the block rebases without conflict, shifts down by exactly the inserted row count, keeps every source value, and leaves the caller's operations untouched |
validation-sparse | every write is applied, probe cells hold the last written values, every rule is retained, and a rule violation is still rejected |
csv-import | row count matches and every imported cell equals the source value for its row and column, including declared number/text types |
undo-large-clear | the clear empties the block and the undos restore every original cell value |
restore-near-ceiling | a valid near-ceiling compressed restore applies its complete block |
paged-evicted-revisit | the band is unloaded before each revisit, the revisit reloads it with the source values, and the revisit asks areColumnsFullyLoaded at least once; areColumnsFullyLoadedCalls per revisit is the accept metric |
node-cold-init | every fresh process loads the module, produces the expected engine result, and reports a plausible heartbeat gap |
Reproduce with:
bun run --filter @sheetwrite/bench bench:core-pathsFormula engine benchmark
countif-100kcriteria-multi-range-editcriteria-range-editcross-sheet-range-editcyclesdiamond-editdistinct-range-editerror-propagationformula-constant-editincremental-dependency-closure-editindependent-first-recomputeindependent-parse-loaditerative-financelet-repeatlet-reuse-editlinear-chainlookup-range-editpercentile-covarianceremoved-sheet-refshared-range-editspill-filter-resizespill-sequence-admissionsum-multi-rangesumifs-3-criteriasumproduct-matrix-editsumproduct-vector-edittext-lookup-manytext-lookup-many-edittext-lookup-many-sortedtext-lookup-many-sorted-edittopology-remove-addunicode-text-date-editvlookup-manyvlookup-many-editvlookup-many-sortedvlookup-many-sorted-editwide-fan-out-editxlookup-manyxlookup-many-editxlookup-many-sortedxlookup-many-sorted-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:formulaDefault and full formula engines
- Shared workloads more than 5% slower on the full engine
- 0 of 8
- Widest paired-ratio spread
- 1.32× (limit 1.50×)
- Default engine WASM, Brotli
- 256,150 B
- Full engine WASM, Brotli
- 344,260 B
The default engine is @sheetwrite/wasm. The full engine is @sheetwrite/formulas. Select it with initSheetwrite(undefined, formulas). Each app uses one engine.
No shared workload tested in the matched rounds had a median paired ratio more than 5% slower with the full engine. This check covers the eight flagged rows, not a new matched run of all 53 rows.
| Shared workload | Size | Default median | Full median | Paired ratio | Ratio p10–p90 |
|---|---|---|---|---|---|
| independent-parse-load | 1,000 | 0.68 ms | 0.67 ms | 1.01× | 0.85×–1.12× |
| independent-first-recompute | 1,000 | 0.64 ms | 0.54 ms | 0.92× | 0.77×–0.98× |
| independent-parse-load | 10,000 | 6.72 ms | 6.57 ms | 0.94× | 0.87×–1.09× |
| independent-first-recompute | 10,000 | 6.44 ms | 5.27 ms | 0.85× | 0.76×–0.93× |
| linear-chain | 8 | 0.01 ms | 0.01 ms | 0.95× | 0.84×–1.09× |
| wide-fan-out-edit | 1,000 | 0.38 ms | 0.35 ms | 0.93× | 0.79×–1.02× |
| scalar-edit-affects-0 | 1,000 | 1 µs | 1 µs | 1.00× | 0.95×–1.06× |
| vlookup-many | 1,000 | 1.59 ms | 1.44 ms | 0.91× | 0.85×–1.02× |
All measurements
Method
The first capture ran all 53 shared workloads on each engine in sequence. Eight rows were more than 5% slower with the full engine. Those eight rows were checked with matched rounds. Each pair runs default, then full, in separate Bun processes. A timed sample starts with 3 untimed warm-up fixtures and then repeats the workload until at least 50 ms of measured time; the sample is the per-iteration mean, so a short operation is measured over many repetitions instead of one cold run. The runner used CPU 4 and one concurrent capture.
The ratio is full time divided by default time within each pair. The table shows the median of 25 paired ratios. The spread is the interpolated 10th to 90th percentile of those ratios. It is not a confidence interval. The time columns are the median times for each engine. A ratio below 1 means the full engine took less time.
Identical shared work is the control for this protocol: the widest paired-ratio p10–p90 span across the 8 rows is 1.32× (limit 1.50×), so each median ratio is supported by its paired samples.
| Shared workload | Iterations per sample |
|---|---|
| independent-parse-load | 64–84 |
| independent-first-recompute | 70–101 |
| independent-parse-load | 7–9 |
| independent-first-recompute | 7–11 |
| linear-chain | 4,177–5,658 |
| wide-fan-out-edit | 117–161 |
| scalar-edit-affects-0 | 81,154–96,796 |
| vlookup-many | 28–40 |
Analysis workloads
Each workload has 1,000 formulas. It has one untimed warmup and five measured samples. Each sample uses a fresh store. The timer measures first recompute. Setup and output reads are outside the timer. Every result is read and summed as a checked checksum. The default engine does not run these analysis functions.
LINEST, SORTBY, and MAP/REDUCE use 100 input rows. DSUM uses 100 database rows, a header, and the criterion key > 0. XIRR uses two cash flows one year apart. The text workload uses three fields. MODE.MULT uses five scalar inputs. MMULT uses two 2-by-2 matrices. Array outputs are reduced to scalar results.
| Full-only workload | Median | p95 |
|---|---|---|
| linest | 4.66 ms | 7.70 ms |
| xirr | 1.54 ms | 1.60 ms |
| dsum | 2.98 ms | 4.16 ms |
| textsplit-regexreplace | 3.07 ms | 3.25 ms |
| sortby | 3.49 ms | 7.23 ms |
| mode-mult | 1.03 ms | 1.31 ms |
| map-reduce | 26.7 ms | 26.9 ms |
| mmult | 1.36 ms | 1.43 ms |
WASM size and Node initialization
| Engine | Node | WASM bytes | Brotli q11 bytes | Cold init median | Cold init p95 |
|---|---|---|---|---|---|
| default | v26.9.0 | 876,918 | 256,150 | 4.03 ms | 4.44 ms |
| full | v26.9.0 | 1,160,345 | 344,260 | 5.33 ms | 5.92 ms |
Raw size is the WASM file size. Brotli size uses quality 11 on that file only. It is not the full package transfer size. Cold initialization uses five fresh Node processes. The timer surrounds initSheetwrite only. Imports and process startup are outside the timer. The operating system file cache is not cleared. No network download is measured. These samples do not establish that the full engine initializes faster.
Raw evidence: bench/results/full-engine-matched-results.json and bench/results/full-engine-results.json. They retain time samples, output checks, allocation samples, source hashes, and WASM hashes. The full workload captures are bench/results/formula-default-results.json and bench/results/formula-full-results.json. Their baseline gates stay blocked in output mode. A valid capture is not a passed timing regression gate.
The landing-page data comes from a separate browser benchmark. The Delivery size section below records published package sizes. Neither data set has an engine comparison field. They are unchanged by this local engine capture.
Reproduce after bun run build:packages:
cd benchbun run bench:formula:enginesbun run bench:formula:matchedXLSX codec benchmark
- Import: time
- 5.5× faster
- Import: peak memory
- 91% less
- Export, 4 scenarios: time
- 1.3–3.7× faster
- Export, 4 scenarios: peak memory
- 19–47% less
The baseline is the same codec at commit 87fadb723979, built from that commit and measured in the same session. Ratio = current ÷ baseline; below 1 is faster or smaller.
| Operation | Scenario | Time ratio | Peak memory ratio |
|---|---|---|---|
| import | combined | 0.18× | 0.09× |
| export | scalar | 0.78× | 0.76× |
| export | rich | 0.69× | 0.80× |
| export | shared-style | 0.74× | 0.53× |
| export | sparse | 0.27× | 0.81× |
All measurements
| Operation | Scenario | Current median | Baseline median | Current peak RSS | Baseline peak RSS |
|---|---|---|---|---|---|
| import | combined | 913.0 ms | 5,033.7 ms | 329.5 MB | 3642.2 MB |
| export | scalar | 300.9 ms | 384.5 ms | 165.3 MB | 217.2 MB |
| export | rich | 217.8 ms | 316.8 ms | 151.4 MB | 188.4 MB |
| export | shared-style | 534.8 ms | 724.2 ms | 163.0 MB | 307.6 MB |
| export | sparse | 27.5 ms | 102.4 ms | 97.0 MB | 120.1 MB |
Reproduce with a checkout of the baseline commit, built and passed as the comparison root:
git worktree add ../sheetwrite-xlsx-baseline 87fadb72397947ea8c6ba576682925c7879579b8cd ../sheetwrite-xlsx-baseline && bun install --frozen-lockfile && bun run build:packagescd - && bun run --filter @sheetwrite/bench bench:xlsx -- --baseline-root ../sheetwrite-xlsx-baselineDelivery size
Registry measurements for every published release. Each delta is measured against the release immediately before it.
v0.5.0v0.5.1
| Package | Tarball | Installed |
|---|---|---|
@sheetwrite/wasm | 352.2 KiB+1.0 KiB+0.3% | 1002.6 KiB+5.5 KiB+0.6% |
@sheetwrite/formulas | 471.1 KiB+908 B+0.2% | 1.26 MiB+5.6 KiB+0.4% |
@sheetwrite/core | 348.8 KiB+8.4 KiB+2.5% | 1.62 MiB+29.8 KiB+1.8% |
@sheetwrite/xlsx | 52.5 KiB+145 B+0.3% | 228.4 KiB+204 B+0.1% |
@sheetwrite/react | 5.9 KiB+176 B+3.0% | 18.6 KiB+1.4 KiB+7.9% |
@sheetwrite/vue | 6.9 KiB+250 B+3.7% | 21.5 KiB+920 B+4.4% |
@sheetwrite/svelte | 5.9 KiB+287 B+5.0% | 16.2 KiB+974 B+6.2% |
v0.4.0v0.5.0
| Package | Tarball | Installed |
|---|---|---|
@sheetwrite/wasm | 351.1 KiB+63.2 KiB+21.9% | 997.1 KiB+204.3 KiB+25.8% |
@sheetwrite/formulas | 470.2 KiB | 1.26 MiB |
@sheetwrite/core | 340.5 KiB+38.3 KiB+12.7% | 1.59 MiB+175.1 KiB+12.0% |
@sheetwrite/xlsx | 52.4 KiB-33.1 KiB-38.7% | 228.2 KiB-201.0 KiB-46.8% |
@sheetwrite/react | 5.7 KiB-3.4 KiB-37.5% | 17.2 KiB-14.7 KiB-46.1% |
@sheetwrite/vue | 6.7 KiB-3.5 KiB-34.2% | 20.6 KiB-15.1 KiB-42.4% |
@sheetwrite/svelte | 5.6 KiB-613 B-9.6% | 15.3 KiB-3.2 KiB-17.2% |
v0.3.1v0.4.0
| Package | Tarball | Installed |
|---|---|---|
@sheetwrite/wasm | 287.9 KiB+390 B+0.1% | 792.8 KiB-2.8 KiB-0.3% |
@sheetwrite/core | 302.1 KiB-215.3 KiB-41.6% | 1.42 MiB-1.29 MiB-47.6% |
@sheetwrite/xlsx | 85.4 KiB-60 B-0.1% | 429.2 KiB-356 B-0.1% |
@sheetwrite/react | 9.2 KiB0 B+0.0% | 31.9 KiB0 B+0.0% |
@sheetwrite/vue | 10.1 KiB-2 B-0.0% | 35.7 KiB0 B+0.0% |
@sheetwrite/svelte | 6.2 KiB0 B+0.0% | 18.5 KiB0 B+0.0% |
v0.3.0v0.3.1
| Package | Tarball | Installed |
|---|---|---|
@sheetwrite/wasm | 287.6 KiB0 B+0.0% | 795.6 KiB0 B+0.0% |
@sheetwrite/core | 517.4 KiB+11 B+0.0% | 2.71 MiB+24 B+0.0% |
@sheetwrite/xlsx | 85.5 KiB0 B+0.0% | 429.6 KiB0 B+0.0% |
@sheetwrite/react | 9.2 KiB0 B+0.0% | 31.9 KiB0 B+0.0% |
@sheetwrite/vue | 10.1 KiB0 B+0.0% | 35.7 KiB0 B+0.0% |
@sheetwrite/svelte | 6.2 KiB0 B+0.0% | 18.5 KiB0 B+0.0% |
v0.2.0v0.3.0
| Package | Tarball | Installed |
|---|---|---|
@sheetwrite/wasm | 287.6 KiB+107.1 KiB+59.4% | 795.6 KiB+305.6 KiB+62.4% |
@sheetwrite/core | 517.4 KiB+145.0 KiB+38.9% | 2.71 MiB+821.8 KiB+42.0% |
@sheetwrite/xlsx | 85.5 KiB+10.5 KiB+14.0% | 429.6 KiB+63.5 KiB+17.3% |
@sheetwrite/react | 9.2 KiB+2.2 KiB+31.2% | 31.9 KiB+9.9 KiB+45.0% |
@sheetwrite/vue | 10.1 KiB+1.5 KiB+17.7% | 35.7 KiB+7.2 KiB+25.3% |
@sheetwrite/svelte | 6.2 KiB+1,002 B+18.7% | 18.5 KiB+3.9 KiB+27.1% |
v0.1.0v0.2.0
| Package | Tarball | Installed |
|---|---|---|
@sheetwrite/wasm | 180.4 KiB+8.2 KiB+4.7% | 489.9 KiB+22.3 KiB+4.8% |
@sheetwrite/core | 372.4 KiB+40.0 KiB+12.0% | 1.91 MiB+209.9 KiB+12.0% |
@sheetwrite/xlsx | 75.0 KiB+53.7 KiB+252.2% | 366.1 KiB+273.1 KiB+293.7% |
@sheetwrite/react | 7.0 KiB+30 B+0.4% | 22.0 KiB+134 B+0.6% |
@sheetwrite/vue | 8.6 KiB-24 B-0.3% | 28.5 KiB-5.5 KiB-16.3% |
@sheetwrite/svelte | 5.2 KiB+66 B+1.2% | 14.5 KiB+296 B+2.0% |
v0.1.0
| Package | Tarball | Installed |
|---|---|---|
@sheetwrite/wasm | 172.3 KiB | 467.6 KiB |
@sheetwrite/core | 332.5 KiB | 1.71 MiB |
@sheetwrite/xlsx | 21.3 KiB | 93.0 KiB |
@sheetwrite/react | 7.0 KiB | 21.9 KiB |
@sheetwrite/vue | 8.6 KiB | 34.0 KiB |
@sheetwrite/svelte | 5.2 KiB | 14.2 KiB |