Skip to content

[Bug]: Severe Performance Lag When Rendering 500+ Rows in Handsontable CE v15.2.0 #1530

@Dhatchanamoorthi8

Description

@Dhatchanamoorthi8

Description

🐞 Bug Description
I am experiencing significant performance issues when rendering large datasets (500+ rows and ~20+ columns) using Handsontable Community Edition v15.2.0. The table becomes extremely sluggish during rendering, scrolling, and editing, especially on mid-range systems and browsers like Chrom

🔢 Handsontable Version
handsontable: 15.2.0

@handsontable/react-wrapper: 15.2.0
(also tested with handsontable@16.0.1, same issue)

🧪 Steps to Reproduce
1.Initialize Handsontable with a dataset of 500+ rows and 20+ columns.

2.Include features like:

  • Cell styles (bgColor, fontWeight, etc.)

  • Merged cells (mergeCells)

  • Conditional formatting

  • React wrapper (@handsontable/react-wrapper)

3.Try to scroll vertically or horizontally.

4.Try to enter or edit data in random cells.

⚙️ Configuration Snippet
` <HotTable
ref={hotRef}
data={sheetData[selectedSheet]}
mergeCells={mergedCells[selectedSheet] || []}
rowHeaders={true}
colHeaders={true}
height="auto"
width="100%"
autoWrapRow={true}
autoWrapCol={true}
colWidths={100}
manualColumnResize={false}
manualRowResize={false}
viewportRowRenderingOffset={20}
viewportColumnRenderingOffset={10}
licenseKey="non-commercial-and-evaluation"
selectionMode="multiple"
formulas={{
engine: hyperFormulaInstance,
sheetName: selectedSheet,
}}
dropdownMenu={false}
filters={false}
outsideClickDeselects={false}
dragToScroll={true}
copyable={true}
copyMode="with-column-group-headers"
className="htCenter"
virtualized={true}
afterChange={afterChange}
beforeChange={(changes, source) => {
if (source === "undo" || source === "redo") {
const hot = hotRef.current?.hotInstance;
changes.forEach(([row, col]) => {
const meta = hot.getCellMeta(row, col);
if (meta.originalStyle) {
hot.setCellMeta(row, col, "style", {
...meta.originalStyle,
});
}
});
}
return true;
}}
afterMergeCells={(cellRange, mergeParent, auto) => {
if (!auto) {
setMergedCells((prevMergedCells) => {
return {
...prevMergedCells,
[selectedSheet]: [
...(prevMergedCells[selectedSheet] || []),
mergeParent,
],
};
});
setIsDirty(true);
}
}}
afterUnmergeCells={(cellRange, auto) => {
if (!auto) {
const { from } = cellRange;

            setMergedCells((prev) => {
              const updated = (prev[selectedSheet] || []).filter(
                (cell) => !(cell.row === from.row && cell.col === from.col)
              );
              return {
                ...prev,
                [selectedSheet]: updated,
              };
            });

            setIsDirty(true);
          }
        }}
        afterSetCellMeta={(row, col, key, value) => {
          handleSetCellMeta(row, col, key, value);
        }}
        cells={(row, col) => {
          try {
            const sheetStyleObj = cellStyles[selectedSheet] || {};
            const basicStyles = Object.values(sheetStyleObj).filter(
              (item) => typeof item === "object" && item?.row !== undefined && item?.col !== undefined
            );
            const generalStyle = basicStyles.find((item) => item.row === row && item.col === col);
            const conditionalStyles = sheetStyleObj.conditionalFormatting || [];
            const conditionStyle = conditionalStyles.find((item) => item.row === row && item.col === col);

            const highlighted = highlightedCells[selectedSheet] || [];
            const isHighlighted = highlighted.includes(`${row}-${col}`);

            return {
              className: generalStyle?.className || '',
              renderer: function (
                instance,
                td,
                row,
                col,
                prop,
                value,
                cellProperties
              ) {
                Handsontable.renderers.TextRenderer.apply(this, arguments);

                const sheetValue = instance.getDataAtCell(row, col);
                const numericValue = Number(sheetValue);
                const isValidNumber = !isNaN(numericValue);


                const meta = instance.getCellMeta(row, col);
                const precision = meta.decimalPrecision ?? decimalPrecisionMap[selectedSheet]?.[`${row}-${col}`];
                const roundedValue = (typeof precision === "number")
                  ? parseFloat(numericValue.toFixed(precision))
                  : numericValue;

                td.innerText = isValidNumber
                  ? (typeof precision === "number" ? roundedValue.toFixed(precision) : String(roundedValue))
                  : String(sheetValue ?? "");
  
                if (sheetValue === null || sheetValue === undefined || sheetValue === "") {
                  td.innerText = "";
                } else if (isValidNumber) {
                  td.innerText = typeof precision === "number"
                    ? numericValue.toFixed(precision)
                    : String(numericValue);
                } else {
                  td.innerText = String(sheetValue);
                }


                let conditionPassed = false;
                if (conditionStyle?.condition && isValidNumber) {
                  const { operator, value: ruleValue } = conditionStyle.condition;
                  const ruleNumber = parseFloat(ruleValue);
                  if (!isNaN(ruleNumber)) {
                    switch (operator) {
                      case "=": conditionPassed = roundedValue === ruleNumber; break;
                      case ">": conditionPassed = roundedValue > ruleNumber; break;
                      case "<": conditionPassed = roundedValue < ruleNumber; break;
                      case ">=": conditionPassed = roundedValue >= ruleNumber; break;
                      case "<=": conditionPassed = roundedValue <= ruleNumber; break;
                      case "!=": conditionPassed = roundedValue !== ruleNumber; break;
                      default: conditionPassed = false;
                    }
                  }
                }

                if (conditionPassed && conditionStyle) {
                  if (conditionStyle.backgroundColor) td.style.backgroundColor = conditionStyle.backgroundColor;
                  if (conditionStyle.fontColor) td.style.color = conditionStyle.fontColor;
                  td.style.fontWeight = conditionStyle.bold ? "bold" : "";
                }


                if (generalStyle) {
                  if (generalStyle.backgroundColor) td.style.backgroundColor = generalStyle.backgroundColor;
                  if (generalStyle.fontColor) td.style.color = generalStyle.fontColor;
                  td.style.fontWeight = generalStyle.bold ? "bold" : "";
                }

     
                if (isHighlighted) {
                  td.style.outline = "2px dashed #ff9900";
                  td.style.outlineOffset = "-2px";
                  td.style.backgroundColor = "rgba(255, 255, 0, 0.3)";
                }

                td.style.overflow = "visible";
                td.style.textOverflow = "clip";
              },


            };
          } catch (error) {
            console.error(error);
          }
        }}

        afterSelection={function (row, col, row2, column2) {
          handleCellSelection(row, col);
          const hot = hotRef.current?.hotInstance;
          const getDataType = hot?.getDataType();
        }}
        contextMenu={true}
        fillHandle={true}
      />`

🖥️ Expected Behavior
Handsontable should handle 500–1000 rows with acceptable performance for:

Initial rendering

Scrolling

Editing input

🐢 Actual Behavior
Initial render takes 2–5 seconds

Typing/editing has a visible input lag

Scrolling is jerky and unresponsive

The lag increases significantly with more merges/styles

🧠 Additional Context
Using conditional styling with cells() or cell meta

Attempted optimization like virtual scrolling or batch rendering (not available in CE)

Tried removing features — marginal improvement

Video or screenshots

No response

Demo

null

HyperFormula version

3.0.0

Your framework

React

Your environment

Chrome ,Windows 11

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions