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