Performance Optimization
Jspreadsheet version 13 introduces a sparse storage engine: cell records are created on demand and organized in pages, so memory and processing scale with the data a worksheet actually contains, not with the size of the grid. A worksheet with 500,000 mostly empty rows costs a fraction of what it did in version 12.
What's new with Version 13
- Sparse storage: cell records are created on demand and organized in pages; memory and processing scale with the data, not with the grid
- Structural operations: insert, delete and move at one million rows run 1.6 to 7 times faster, and sorting large columns 84 to 270 times faster
- Rendering: border elements are materialized only inside the viewport, scroll events resolve in under a millisecond, and the stylesheet only rebuilds when a new style appears
Key Features
- Sparse storage: Cell records exist only where there is data; blank areas of the grid cost nothing
- Virtual DOM: Renders only visible cells for optimal performance
- Lazy borders: Selection and border elements are only materialized inside the viewport
- Viewport Management: Controls visible area for efficient rendering
The architecture in one paragraph
Version 12 stores the worksheet as a dense matrix: every cell exists as an object, and often a DOM element, whether it holds data or not. Version 13 stores sparse paged records: cell objects are created only when a cell needs identity, a value, a formula, a style of its own or a comment, organized in pages of 256 rows resolved by index arithmetic. The DOM is created only for the visible viewport. On top of that sit dedicated index structures, a prefix-height index for scrolling, a canonical cache for style deduplication, indexed plugin hooks and memoized nested-header maps, so that the common operations cost what they touch, not what the grid contains. The same engine runs headless on a server with no DOM at all, producing byte-identical documents.
The practical consequence: a 100,000 by 26 worksheet in version 13 behaves like a small one unless you genuinely operate on 100,000 cells, and even then the batch paths are optimized end to end, including undo and redo.
Benchmarks against version 12
The numbers below come from the benchmark suite of the Jspreadsheet repository, run on Node.js with jsdom on a single thread. Absolute times vary with hardware; the ratios are what matter. Where a scenario exists on both engines, the same script was run against version 12 and version 13.
Sorting
100,000 data rows unless noted. Version 13 collects and coerces values once per row instead of on every comparison, and scans only the occupied sparse bounds.
| Scenario | v12 | v13 | Speedup |
|---|---|---|---|
| Sort 100,000 numbers | 6,456 ms | 45 ms | 143x |
| Sort 100,000 strings | 10,636 ms | 127 ms | 84x |
| Sort 20,000 dropdown cells by label | 496 ms | 9.3 ms | 53x |
| Re-sort an already sorted 100,000-row column | 1,621 ms | 6 ms | 270x |
Undo of a 100,000-row sort: 17.8 ms. Redo: 29.3 ms. Multi-column sorting, orderBy([{ column, direction }, ...]), has the same performance profile, and the exact resulting order travels to connected users so every environment agrees byte for byte.
Styles
100,000-row worksheet. Version 13 deduplicates styles into shared ids through a canonical cache, stores full column and row styles as a single entry on the column or row itself, and only rebuilds the CSS when an operation introduces a style that never existed before.
| Scenario | v12 | v13 | Speedup |
|---|---|---|---|
| 1,000 single-cell setStyle calls, thousands of styles present | 1,495 ms | 10.6 ms | 140x |
| Style a whole column of a 100,000-row sheet | 59.4 ms | 3.3 ms | 18x |
| Reset a whole column style on a sparse sheet | crashes | 0.31 ms |
Version 13 at scale:
| Scenario | v13 |
|---|---|
| setStyle batch, 10,000 cells | 41 ms |
| Undo and redo of that batch | 17 ms and 22 ms |
| setStyle batch, 5,000 unique new styles | 19 ms |
| Full style extraction, 16,000 entries | 8.0 ms |
| Copy 100,000 cells, half of them styled | 28 ms |
| Paste 100,000 cells carrying styles | about 1.1 s through the value pipeline, undo 175 ms |
The batch numbers include the collaboration-aware history: each individually addressed cell records its identity so the undo stays exact across concurrent structural changes, about 1 µs per cell. Column and row styles bypass the per-cell path entirely: one entry regardless of the sheet height, inherited by the cells when they render, copied when they are copied, and reported by getStyle whether they were ever rendered or not.
Structural operations
1,000,000-row grid with 100,000 data rows, one operation at the worst position, the top, where every position shifts. Version 13 repairs coordinates only from the first affected position, removes rows in one compaction pass whose cost is independent of how scattered the deleted rows are, and bounds every column walk by the occupied area instead of the grid height.
| Scenario, 1,000,000-row grid | v12 | v13 | Speedup |
|---|---|---|---|
| Insert 1 row at the top | 85 ms | 52 ms | 1.6x |
| Delete 1 row at the top | 91 ms | 50 ms | 1.8x |
| Delete 100 rows, and undo the delete | 90 ms and 134 ms | 50 ms and 74 ms | 1.8x |
| Insert 1 column | 116 ms | 36 ms | 3.2x |
| Delete 1 column | 267 ms | 53 ms | 5x |
| Move a row | 90 ms | 42 ms | 2.1x |
| Move a column, and undo | 329 ms and 222 ms | 46 ms and 54 ms | 7.2x and 4.1x |
| Initialization | 246 ms, 169 MB | 165 ms, 116 MB |
Moves repair only their own window; nothing after the highest affected position is touched. Operations away from the top are cheaper still, so appending or deleting near the end of a million-row sheet costs milliseconds. Headless, on a server, the same operations run in about 16 ms; the numbers above include the browser DOM work. Undo and redo of structural operations restore the exact document, including the row and column identity that connected users agree on.
Scrolling and navigation
1,000,000-row worksheet. Version 13 resolves the first visible row through a prefix-height index instead of walking row heights.
| Scenario | v12 | v13 |
|---|---|---|
| Scroll event, fixed position, 1M rows | 15 to 26 ms | 0.01 ms |
| Scroll event, changing position, 1M rows | 15 to 26 ms | 0.15 to 0.72 ms |
Version 13 also implements the full Excel block-navigation contract for Ctrl + Arrow, with value blocks, gaps, merged cells and hidden rows. Jumping across 1,000,000 blank rows resolves in 0.36 ms thanks to a sparse-bounds short-circuit.
Editing and history
Version 12's history recorder copies the entire action array on every operation, so sessions get slower the longer they run. Version 13 truncates in place.
| Scenario | Behaviour |
|---|---|
| v12, 20,000-edit session | 79 µs per operation and climbing |
| v13, 40,000-edit session | 0.91 µs per operation, flat |
A single setValue in version 13 takes 1.7 µs. History also became exact: undoing a style, a sort or a structural operation restores the precise prior document, inheritance relationships included, rather than a visually similar copy. The collaboration-aware history costs nothing on the editing hot path: identity references are captured as plain pointers at record time, and the replay payloads are built only when undo or redo actually runs.
Formulas
50,000-row ranges.
| Scenario | v12 | v13 | |
|---|---|---|---|
| First evaluation of SUM over 50,000 rows | 31 ms | 6.8 ms | 4.6x |
| Edit a cell inside a 50,000-row SUM range | 4.0 ms | 4.3 ms | parity |
Version 13 at scale:
| Scenario | v13 |
|---|---|
| executeFormula call overhead | 1.2 µs |
| setFormula batch | 3.3 µs per formula |
| Dependency chain wiring | 4.2 µs per formula |
| 2,000-level dependency cascade recalculation | 2 ms |
| One edit fanning out to 5,000 dependents | 5 ms |
| Formula tokenizer | about 0.2 µs per formula |
The dependency graph engine is validated for topological ordering at 500,000 nodes.
Search
Version 13 resolves displayed labels through a single fast path and gates the scan on record existence in the sparse store: searching 60,000 cells with few hits takes 1.10 ms, with plain data at 0.95 ms, dropdown labels at 0.54 ms and formula results at 1.65 ms. Search matches what the user sees, dropdown labels, masked numbers and formula results, not just raw values.
Validations and conditional formatting
Validations in version 13 are rectangle math over the sparse store, never per-cell pointers.
| Scenario | v12 | v13 |
|---|---|---|
| hasErrors() with a 20-cell rule on a 20,000-row sheet | 103 ms | 0.34 ms |
| List-validation check against a 10,000-cell source | 907 µs | 10 µs, cached and chain-invalidated |
| Heap for a 20,000-row validated build | 14 MB | 5 MB |
Applying a validation to a 500,000-row sparse column takes 1.4 ms.
Data, import and export
60,000 cells.
| Scenario | v13 |
|---|---|
| setData, 60,000 cells | 25.7 ms |
| getData raw, computed and unformatted | 1.3 ms, 137x faster than formatted extraction |
| CSV export with repeated masked values | 9.6 ms, from 250 ms before the render-mask memo, 27x |
| Copy 60,000 cells | 14 ms |
| Paste 60,000 cells through the full value pipeline | 400 ms, undo instant |
| Fill-handle drag, 50,000 rows, copy and numeric trend | 81 ms and 49 ms |
| Double-click autofill, 20,000 rows | 20 ms |
| setMeta batch, 10,000 entries | 4.6 ms |
Server-side operation
Version 13 runs the identical engine without a DOM. A server replaying a client's operations applies them at 0.016 ms per operation, against roughly 300 ms to serialize a full 20,000-row document of about 79 MB per change with a store-the-whole-file approach. Styles, sorting, formulas, structural operations and their undo histories all produce byte-identical saved documents whether they ran in a browser or on the server; the test suite enforces this with headless parity gates per module.
Performance Optimization Settings
Spreadsheet Level
| Property | Description | Type |
|---|---|---|
tableOverflow |
Enable scrollable viewport for large datasets | boolean |
tableWidth |
Set fixed width for the viewport | string |
tableHeight |
Set fixed height for the viewport | string |
fullscreen |
Automatically adjust grid to use full screen space | boolean |
Worksheet level
| Property | Description | Type |
|---|---|---|
pagination |
Number of rows to display per page | number |
Examples
Large Dataset Example
Create a spreadsheet with one billion cells (10,000 columns × 100,000 rows):
<html>
<script src="https://jspreadsheet.com/v13/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<link rel="stylesheet" href="https://jspreadsheet.com/v13/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons" />
<div id="spreadsheet"></div>
<p id="console"></p>
<script>
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('YThmYmYwNzE1YmM1MWRjZDk1YTA2NzMzYjFiOWZhZjY5Y2U4N2YzMzVhNmQ5NDcyZDEzOTE3Y2VkZTdlMGY5NTQxOWI5MThmM2IyNWJhMGRiNjc2ZTE4ZmEyYTI2YTA5MmQxZmUwOWZjMTcxNmJjNjkwNDNmNzcyYjg0MDBkNTIsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlOelV3TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Initial time before creating the table
let s = Date.now();
// Create the table
jspreadsheet(document.getElementById('spreadsheet'), {
tableOverflow: true,
tableWidth: '600px',
tableHeight: '300px',
worksheets: [{
minDimensions: [10000,100000],
}],
onload: function() {
// Final time
let e = Date.now();
// Update console
document.getElementById('console').innerText = 'The table was created in: ' + (e - s) + 'ms';
}
})
</script>
</html>
import React, { useRef } from "react";
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/react";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('YThmYmYwNzE1YmM1MWRjZDk1YTA2NzMzYjFiOWZhZjY5Y2U4N2YzMzVhNmQ5NDcyZDEzOTE3Y2VkZTdlMGY5NTQxOWI5MThmM2IyNWJhMGRiNjc2ZTE4ZmEyYTI2YTA5MmQxZmUwOWZjMTcxNmJjNjkwNDNmNzcyYjg0MDBkNTIsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlOelV3TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Create a new data grid
export default function App() {
// Spreadsheet array of worksheets
const spreadsheet = useRef();
const console = useRef();
// Initial time before creating the table
let s = Date.now();
// When the data grid is ready
const onload = () => {
// Final time
let e = Date.now();
// Update console
console.current.innerText = 'The table was created in: ' + (e - s) + 'ms';
};
// Render data grid component
return (
<>
<Spreadsheet ref={spreadsheet} onload={onload} tableOverflow={true} tableWidth="600px" tableHeight="300px">
<Worksheet minDimensions={[10000, 100000]} />
</Spreadsheet>
<div ref={console}></div>
</>
);
}
<template>
<Spreadsheet @onload="onload" tableOverflow tableWidth="600px" tableHeight="300px">
<Worksheet :minDimensions="minDimensions" />
</Spreadsheet>
<div ref="console"></div>
</template>
<script>
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/vue";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('YThmYmYwNzE1YmM1MWRjZDk1YTA2NzMzYjFiOWZhZjY5Y2U4N2YzMzVhNmQ5NDcyZDEzOTE3Y2VkZTdlMGY5NTQxOWI5MThmM2IyNWJhMGRiNjc2ZTE4ZmEyYTI2YTA5MmQxZmUwOWZjMTcxNmJjNjkwNDNmNzcyYjg0MDBkNTIsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlOelV3TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
export default {
components: {
Spreadsheet,
Worksheet,
},
data() {
return {
minDimensions: [10000, 100000],
startTime: null,
};
},
methods: {
onload() {
const endTime = Date.now();
this.$refs.console.innerText = 'The table was created in: ' + (endTime - this.startTime) + 'ms';
},
},
mounted() {
this.startTime = Date.now();
},
};
</script>
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import jspreadsheet from 'jspreadsheet';
import "jspreadsheet/dist/jspreadsheet.css";
import "jsuites/dist/jsuites.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('YThmYmYwNzE1YmM1MWRjZDk1YTA2NzMzYjFiOWZhZjY5Y2U4N2YzMzVhNmQ5NDcyZDEzOTE3Y2VkZTdlMGY5NTQxOWI5MThmM2IyNWJhMGRiNjc2ZTE4ZmEyYTI2YTA5MmQxZmUwOWZjMTcxNmJjNjkwNDNmNzcyYjg0MDBkNTIsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlOelV3TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Initial time before creating the table
let s = Date.now();
@Component({
standalone: true,
selector: 'app-root',
template: `<div #spreadsheet></div><div id="console"></div>`,
})
export class AppComponent implements AfterViewInit {
@ViewChild('spreadsheet') spreadsheet: ElementRef;
// Worksheets
worksheets: jspreadsheet.worksheetInstance[];
// Create a new data grid
ngAfterViewInit() {
// Create spreadsheet
this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
tableOverflow: true,
tableWidth: '600px',
tableHeight: '300px',
worksheets: [
{
minDimensions: [10000, 100000],
},
],
onload: function () {
// Final time
let e = Date.now();
// Update console
let console = document.getElementById('console');
if (console) {
console.innerText = 'The table was created in: ' + (e - s) + 'ms';
}
},
});
}
}
See Also
- Viewport - Virtual scrolling for large datasets
- Performance Demo - Live demo with 10000x100000 cells
- Table Overflow - Enable scrolling for large spreadsheets