Migration from 13.1.0 to 15.1.0 reduces performance (API core getData degraded?)

Hello devs and supporters. In our project we needed to update to the latest version of the library because we have a bug related to the inability to copy from locked cells. This bug was fixed in 14.*
(Handsontable is a JavaScript data grid that looks and feels like a spreadsheet - Available for React, Angular, and Vue), but in the major version 14 of the library a regression occurred
(Handsontable is a JavaScript data grid that looks and feels like a spreadsheet - Available for React, Angular, and Vue) related to fast editing. That’s why I waited for the release of version 15, where this issue has already been resolved.

Now I have encountered a severe performance drop. When initializing the table, the following block of code is executed:

useEffect(() => {
  if (
    vaultLoadDataStatus === 'IN_PROCESS' ||
    inputValues.length === 0 ||
    hot === null
  )
    return
  console.time('updateInputResultProps')
  updateInputResultProps(hot)
  console.timeEnd('updateInputResultProps')
}, [vaultLoadDataStatus, inputValues, plantsListForAside, hot])

In library version 13.1.0 the function updateInputResultProps executes in about ~230 ms, while in library version 15.1.0 it takes about ~6700 ms—a slowdown by a factor of 30!

Below is the implementation of the function:

updateInputResultProps = (hot: Handsontable) => {
  this.vaultSpreadsheet.inputResultProps =
    this.vaultSpreadsheet.colNumberPerStation.map((stationColNum, idx) => {
      const sumIdx = [...this.inputResultCellIndexes].splice(idx * 3, idx * 3 + 3)
      const plantFullData = this.plantsData.find(
        (el) => el.plantId === this._displayedPlants[idx]?.plantId
      )
      const viewOnly = this._displayedPlants[idx]?.viewOnly ?? false
      const FLOOD_MODE_WATCH =
        this.floodsLeft?.some(
          (id) => id === this._displayedPlants[idx]?.plantId
        ) ?? false
      const valueMin = FLOOD_MODE_WATCH
        ? `=${calcCellFromAlphabet(sumIdx[0] + 1)}25`
        : this.inputValues[idx]?.W_MIN
      const valueMax = FLOOD_MODE_WATCH
        ? `=${calcCellFromAlphabet(sumIdx[2] + 1)}25`
        : this.inputValues[idx]?.W_MAX
      const valueGen = FLOOD_MODE_WATCH
        ? `=${calcCellFromAlphabet(sumIdx[2] + 1)}25`
        : this.inputValues[idx]?.P_GEN_TARGET
      const isWMin = this.inputValues[idx]
        ? Object.keys(this.inputValues[idx])?.some((el) => el === 'W_MIN')
        : false
      const isWMax = this.inputValues[idx]
        ? Object.keys(this.inputValues[idx])?.some((el) => el === 'W_MAX')
        : false
      const isPGen = this.inputValues[idx]
        ? Object.keys(this.inputValues[idx])?.some(
            (el) => el === 'P_GEN_TARGET'
          )
        : false
      const inputValuesRes: Omit<
        IVaultStore['inputValues'][0],
        'plantId'
      > = {
        W_MIN: isWMin ? valueMin : undefined,
        W_MAX: isWMax ? valueMax : undefined,
        P_GEN_TARGET: isPGen ? valueGen : undefined,
      }
      if (FLOOD_MODE_WATCH) {
        console.time('FLOOD_MODE_WATCH')
        if (plantFullData?.plantOptimized) {
          inputValuesRes['W_MIN'] = hot.getData()[24][sumIdx[0]]
          inputValuesRes['W_MAX'] = hot.getData()[24][sumIdx[2]]
        } else {
          inputValuesRes['P_GEN_TARGET'] = hot.getData()[24][sumIdx[2]]
        }
        console.timeEnd('FLOOD_MODE_WATCH')
      }

      console.time('validatePGenTarget')
      const validate_P_GEN = validatePGenTarget(
        hot,
        inputValuesRes,
        {
          pMin: sumIdx[0],
          pGen: sumIdx[1],
          pMax: sumIdx[2],
        },
        isPGen
      )
      console.timeEnd('validatePGenTarget')

      console.time('validateWMin')
      const validate_W_MIN = validateWMin(
        hot,
        inputValuesRes,
        {
          pMin: sumIdx[0],
          pGen: sumIdx[1],
          pMax: sumIdx[2],
        },
        isWMin
      )
      console.timeEnd('validateWMin')

      console.time('validateWMax')
      const validate_W_MAX = validateWMax(
        hot,
        inputValuesRes,
        {
          pMin: sumIdx[0],
          pGen: sumIdx[1],
          pMax: sumIdx[2],
        },
        isWMax
      )
      console.timeEnd('validateWMax')
      const isErrorPGENTARGET = typeof validate_P_GEN !== 'string'
      const isErrorWMIN = typeof validate_W_MIN !== 'string'
      const isErrorWMAX = typeof validate_W_MAX !== 'string'
      return {
        stationColNum: stationColNum || 0,
        wMin: {
          active: isWMin,
          disabled:
            this._displayedPlants[idx]?.accepted ||
            viewOnly ||
            this._isLastDay ||
            FLOOD_MODE_WATCH ||
            plantFullData?.parameters?.E_MAX_E_MIN?.value.turnedOn ||
            !this._editMode ||
            this._isFinishStage,
          value: valueMin,
          isValid: isErrorWMIN,
          comment:
            validate_W_MIN !== undefined
              ? getStyledComment(validate_W_MIN)
              : undefined,
        },
        pGen: {
          active: isPGen,
          disabled:
            this._displayedPlants[idx]?.accepted ||
            viewOnly ||
            this._isLastDay ||
            FLOOD_MODE_WATCH ||
            !this._editMode ||
            this._isFinishStage,
          value: valueGen,
          isValid: isErrorPGENTARGET,
          comment:
            validate_P_GEN !== undefined
              ? getStyledComment(validate_P_GEN)
              : undefined,
        },
        wMax: {
          active: isWMax,
          disabled:
            this._displayedPlants[idx]?.accepted ||
            viewOnly ||
            this._isLastDay ||
            FLOOD_MODE_WATCH ||
            !this._editMode ||
            this._isFinishStage,
          value: valueMax,
          isValid: isErrorWMAX,
          comment:
            validate_W_MAX !== undefined
              ? getStyledComment(validate_W_MAX)
              : undefined,
        },
      }
    })
}

I tried to localize the problem and find the bottleneck that caused the slowdown. Apparently, the delay is linked to retrieving data from the table by calling the table instance’s API (hotUp.getData()), which is present in all of the validator functions.

For example:

const validateWMin = (
  hotUp: Handsontable | null,
  inputValues: Omit<IVaultStore['inputValues'][0], 'plantId'>,
  sumIdx: IValidateSumIdx & { pGen: number },
  active: boolean
) => {
  console.time('tableDataUp')
  const tableDataUp = hotUp?.getData()?.slice(24, 25)[0] ?? []
  console.timeEnd('tableDataUp')
  // other lines of code
}

const validateWMax = (
  hotUp: Handsontable | null,
  inputValues: Omit<IVaultStore['inputValues'][0], 'plantId'>,
  sumIdx: IValidateSumIdx & { pGen: number },
  active: boolean
) => {
  console.time('validateWMax_tableDataUp')
  const tableDataUp = hotUp?.getData()?.slice(24, 25)[0] ?? []
  console.timeEnd('validateWMax_tableDataUp')
  // other lines of code
}

const validatePGenTarget = (
  hotUp: Handsontable | null,
  inputValues: Omit<IVaultStore['inputValues'][0], 'plantId'>,
  sumIdx: IValidateSumIdx & { pGen: number },
  active: boolean
) => {
  console.time('validatePGenTarget_tableDataUp')
  const tableDataUp = hotUp?.getData()?.slice(24, 25)[0] ?? []
  console.timeEnd('validatePGenTarget_tableDataUp')
  // other lines of code
}

I wrapped each line

const tableDataUp = hotUp?.getData()?.slice(24, 25)[0] ?? []

in a timer in the validator functions and observed the following behavior.

13.1.0

15.1.0

Slowing down the execution of each hotUp?.getData() call by almost 30 times

What is causing such a slowdown of the getData method? How can this problem be solved?

I made some optimizations. Instead of using
const tableDataUp = hotUp?.getData()?.slice(24, 25)[0] ?? []
in each validator function, I retrieve the data once at the updateInputResultProps
( const rowData = hot.getDataAtRow(24) ?? [] )
and pass it down to the validator functions. With this change, I was able to reduce the total execution time of updateInputResultProps to <40ms, which is even faster than the old implementation on version 13.1.0.

15.1.0

On 13.1.0 even faster

However, even with these optimizations, the first table render is still slower (~7s) compared to the old validation implementation + version 13.1.0 (~4s). It feels like the problem with slowdown is not only on the getData side.

P.S. I’m using node v18.20.4. FYI

Hi @mr.fursy

Thank you for providing such a detailed explanation and sharing the code. It’s really helpful in understanding the issue. However, I still need a demo or a specific step-by-step guide to replicate the problem on my end. Could you provide that?

In general we run scrolling, edition, navigation performance tests (with stats and logs) for each version, so I believe that there are some specified settings that are used in your application that might have impact on the end result.

Hi again, I can’t provide a step-by-step reproducible example because there is too much legacy code here. However, I inspected the code in the Performance tab (Chrome DevTools) and discovered that the functions collapseColumns, initGrid, and the overall logic in the useEffect that runs during table initialization (see the screenshot) are taking too long to execute.


I wrapped all the logic inside initGrid with console.time/console.timeEnd and noticed it takes approximately ~1300 ms. Then I started wrapping individual pieces of the logic inside initGrid with time/timeEnd and saw that the line

collapseColumns(hot, collapsibleColumns, headersMap)

takes about ~1500 ms. I wrapped the collapseColumns code block with time/timeEnd:

const collapseColumns = (
  hot: Handsontable,
  collapsibleColumns: GridSettings['collapsibleColumns'],
  headersMap: number[],
) => {
  if (collapsibleColumns && (collapsibleColumns as DetailedSettings[])?.length) {
    const collapsible: number[] = []
    (collapsibleColumns as DetailedSettings[]).forEach((col) => {
      hot.getPlugin('collapsibleColumns').collapseSection(col)
      collapsible.push(headersMap[col.col])
    })
    localStorage.setItem('collapsibleColumns', JSON.stringify(collapsible))
  }
}

const initGrid = (
  element: HTMLDivElement,
  options: GridSettings,
  setHot: Dispatch<Handsontable>,
  viewOnly: boolean,
  headersMap: number[],
  collapsibleColumns?: GridSettings['collapsibleColumns'],
) => {
  const hot = new Handsontable(element, options)
  const cells = options.cell || []
  collapseColumns(hot, collapsibleColumns, headersMap)

  hot.updateSettings({
    cells: (row: number, col: number) =>
      renderCells(row, col, cells, viewOnly),
    afterColumnCollapse: (...ars) => setColumnCollapse(...ars, headersMap),
    afterColumnExpand: (...ars) => setColumnCollapse(...ars, headersMap),
  })

  setHot(hot)
}

// Executes on the first render (initialization) of the table
useEffect(() => {
  if (refContainer?.current && (nestedHeaders.length > 0 || colHeaders)) {
    const optionsGrid: GridSettings = {
      data: spreadsheetData,
      columns: columnsFinal.map((el) => ({
        ...el,
        editor: el.editor === false ? NonEditableEditor : 'numeric',
      })),
      className: className,
      beforePaste: beforePaste || defaultBeforePaste,
      beforeAutofill: beforeAutofill || defaultBeforeAutofill,
      colHeaders,
      comments: true,
      rowHeaders: rowHeaders.length !== 0 ? rowHeaders : finalRowHeaders,
      afterGetColHeader: handleAfterGetColHeader,
      afterSelectionEnd,
      height,
      width,
      hiddenColumns: true,
      colWidths,
      ...(nestedHeaders.length > 0 && {
        nestedHeaders: TEST_MODE
          ? [
              ...nestedHeaders,
              data[0].map((_, index: number) => `${calcCellFromAlphabet(index + 1)}`),
            ]
          : nestedHeaders,
      }),
      rowHeights: 15,
      collapsibleColumns:
        (collapsibleColumns as DetailedSettings[]).length > 0 &&
        nestedHeaders.length > 0
          ? TEST_MODE
            ? (collapsibleColumns as DetailedSettings[]).map((el) => ({
                ...el,
                row: el.row - 1,
              }))
            : collapsibleColumns
          : false, // explicitly disable the plugin if there are no nested headers
      licenseKey: LICENSE_KEY,
      selectionMode: 'multiple',
      formulas: {
        engine: hyperformulaInstance,
      },
      beforeRenderer,
      cell: cellFinal,
      beforeChange: handleBeforeChange,
      afterChange: handleAfterChange,
      afterSelection,
      afterDeselect,
      beforeKeyDown,
      afterGetRowHeader,
      rowHeaderWidth,
      maxRows: spreadsheetData.length,
    }
    initGrid(
      refContainer?.current,
      { ...optionsGrid, language: ruRU.languageCode },
      handleSetHot,
      viewOnly,
      headersMap.current,
      collapsibleColumns,
    )
    if (toggleAllRef.current) {
      toggleAllRef.current?.addEventListener('click', toggleAllHeaders)
    }
    setIsInitSpreadsheet && setIsInitSpreadsheet(true)
  }
  return () => {
    refContainer.current = null
    handleSetHot(null)
    if (toggleAllRef.current) {
      toggleAllRef.current?.removeEventListener('click', toggleAllHeaders)
    }
  }
}, [])

// Executes when the table is updated
useLayoutEffect(() => {
  if (refContainer?.current && (nestedHeaders.length > 0 || colHeaders)) {
    const optionsGrid: GridSettings = {
      data: spreadsheetData,
      columns: columnsFinal.map((el) => ({
        ...el,
        editor: el.editor === false ? NonEditableEditor : 'numeric',
      })),
      height,
      width,
      cell: cellFinal,
      cells: (row, col) => renderCells(row, col, cellFinal, viewOnly),
      afterChange: handleAfterChange,
      beforePaste: beforePaste || defaultBeforePaste,
      beforeAutofill: beforeAutofill || defaultBeforeAutofill,
      afterGetColHeader: handleAfterGetColHeader,
      afterSelectionEnd,
      beforeKeyDown,
    }

    if (hotRef.current) {
      // Before updating nestedHeaders, save the indexes of the expanded cells
      // Rename the variable to avoid name collisions with the prop
      const collapsibleCols =
        JSON.parse(localStorage.getItem('collapsibleColumns') as string) ?? []
      const collapsible: DetailedSettings[] = []

      // Before updating, expand all columns so that the new values are set correctly
      if (isUpdateNestedHeaders) {
        optionsGrid.nestedHeaders = nestedHeaders
        hotRef.current.getPlugin('collapsibleColumns').expandAll()
      }

      // Update the table settings
      hotRef.current.updateSettings(optionsGrid)

      // After the update, restore the state of the collapsed columns
      if (isUpdateNestedHeaders && Array.isArray(collapsibleColumns)) {
        collapsibleCols.forEach((col: number) => {
          const columnIdx = headersMap.current.findIndex(
            (nestedHeaderIdx) => nestedHeaderIdx === col
          )

          if (columnIdx !== -1) {
            const collapsibleColumn = collapsibleColumns.find(
              (item) => item.col === columnIdx
            )

            if (collapsibleColumn) {
              collapsible.push({
                row: collapsibleColumn.row,
                col: columnIdx,
                collapsible: true,
              })
            }
          }
        })

        collapseColumns(hotRef.current, collapsible, headersMap.current)
      }
    }
  }
}, [
  refContainer,
  height,
  width,
  columnsFinal,
  nestedHeaders,
  data,
  collapsibleColumns,
  cell,
  afterChange,
  spreadsheetData,
  afterGetColHeader,
])

I found a method called batch that allows combining several operations into a single render, and I implemented it as follows:

const collapseColumns = (
  hot: Handsontable,
  collapsibleColumns: GridSettings['collapsibleColumns'],
  headersMap: number[],
) => {
  if (collapsibleColumns && (collapsibleColumns as DetailedSettings[])?.length) {
    const collapsible: number[] = []
    const plugin = hot.getPlugin('collapsibleColumns')
    const columnsToCollapse = collapsibleColumns as DetailedSettings[]

    // Disable re-rendering during the operations
    hot.batch(() => {
      for (const element of columnsToCollapse) {
        const col = element
        plugin.collapseSection(col)
        collapsible.push(headersMap[col.col])
      }
    })

    localStorage.setItem('collapsibleColumns', JSON.stringify(collapsible))
  }
}

It looks odd—like a hack.

Using the following timers:

const initGrid = (
  element: HTMLDivElement,
  options: GridSettings,
  setHot: Dispatch<Handsontable>,
  viewOnly: boolean,
  headersMap: number[],
  collapsibleColumns?: GridSettings['collapsibleColumns'],
) => {
  console.time('initGrid')
  const hot = new Handsontable(element, options)
  const cells = options.cell || []
  console.timeEnd('initGrid')

  console.time('collapseColumns')
  collapseColumns(hot, collapsibleColumns, headersMap)
  console.timeEnd('collapseColumns')

  console.time('updateSettings')
  hot.updateSettings({
    cells: (row: number, col: number) =>
      renderCells(row, col, cells, viewOnly),
    afterColumnCollapse: (...ars) => setColumnCollapse(...ars, headersMap),
    afterColumnExpand: (...ars) => setColumnCollapse(...ars, headersMap),
  })

  setHot(hot)
  console.timeEnd('updateSettings')
}

I obtained the following results:

initGrid: 190.781982421875 ms
collapseColumns: 211.69287109375 ms
updateSettings: 165.81884765625 ms
initGrid: 32.54296875 ms
collapseColumns: 0.002197265625 ms
updateSettings: 20.9921875 ms

It seems to me that it should be possible to speed things up and make the logic in initGrid more efficient. The following blocks:

const hot = new Handsontable(element, options)
const cells = options.cell || []

and

hot.updateSettings({
  cells: (row: number, col: number) => renderCells(row, col, cells, viewOnly),
  afterColumnCollapse: (...ars) => setColumnCollapse(...ars, headersMap),
  afterColumnExpand: (...ars) => setColumnCollapse(...ars, headersMap),
})

are taking too long to execute. After updating the collapseColumns function, the performance snapshot looks like this.

Can you suggest any improvements or ideas on how to optimize this further?

Oh gosh. I also noticed that the response after editing a cell in my table dropped by 3 times (the logic that is executed for the afterChange hook). On 13.1.0 it’s about ~0.5s, on 15.1.0 ~1.5s.

Hi @mr.fursy

Could you please check the latest pre-release https://www.npmjs.com/package/handsontable?activeTab=versions.

We notices that it improves loading of larger datasets. With autoRowSize on it’s even 10 times faster than 15.1.0.

Good day. I compared 13.1.0 (with hyperformula 2.6.0) and 15.2.0 (with hyperformula 3.0.0) and this is what I noticed. The collapseColumns function, which is called in the initGrid function, which is called in the useEffect responsible for initializing the table, runs for 2.2-2.5 seconds in 15.2.0 versus ~950 ms in 13.1.0

const collapseColumns = (
  hot: Handsontable,
  collapsibleColumns: GridSettings['collapsibleColumns'],
  headersMap: number[],
) => {
  if (collapsibleColumns && (collapsibleColumns as DetailedSettings[])?.length) {
    const collapsible: number[] = []

    ;(collapsibleColumns as DetailedSettings[]).forEach((col) => {
      hot.getPlugin('collapsibleColumns').collapseSection(col)
      collapsible.push(headersMap[col.col])
    })
    localStorage.setItem('collapsibleColumns', JSON.stringify(collapsible))
  }
}
const initGrid = (
  element: HTMLDivElement,
  options: GridSettings,
  setHot: Dispatch<Handsontable>,
  viewOnly: boolean,
  headersMap: number[],
  collapsibleColumns?: GridSettings['collapsibleColumns'],
) => {
  const hot = new Handsontable(element, options)
  const cells = options.cell || []
  const columns = options.columns || []
  collapseColumns(hot, collapsibleColumns, headersMap)

  hot.updateSettings({
    cells: (row: number, col: number) => renderCells(row, col, cells, viewOnly, columns.length),
    afterColumnCollapse: (...ars) => setColumnCollapse(...ars, headersMap),
    afterColumnExpand: (...ars) => setColumnCollapse(...ars, headersMap),
  })

  setHot(hot)
}

This code snippet runs in version 13.1.0 ~100ms, and in version 15.2.0 ~400ms.

const ht = new Handsontable(document.createElement('div'), {
      data: this.vaultSpreadsheet.data,
      licenseKey: 'non-commercial-and-evaluation',
      formulas: {
        engine: HyperFormula.buildEmpty({
          licenseKey: 'internal-use-in-handsontable',
        }),
      },
    })

Hi @mr.fursy

Yes, 15.2.0 might be slower, but we made some performance fixes in the upcoming version.

Please try to run this demo https://jsfiddle.net/xy5ewjpu/1/ , add your settings and compare.

I also encountered a strange bug on 15 major version (it is on 15.0.0, 15.1.0, 15.2.0). I can’t enter a value in the last few rows of the table (19:24 rows, center column).


It’s as if the cell is blocked for editing. I don’t notice such a bug on 13.1.0 and 14.6.0.

That is interesting. One of our Clients reported an issue that he wasn’t able to edit cells the last column of the grid. But I wasn’t able to replicate the same issue on my device. It might be related to some certain settings (they also did not share any demo).

I removed the height property from the component, and the cells became editable. Strange…

I also noticed that the height of the lines has changed in another place. In our project, the line height is determined via css (15px height), since the library does not allow you to set the line height below 23px using its api. To align with the table for proper virtualization, I specified rowHeights: 15. But after the update, the row height became 23px.

const defaultSettings: Partial<Handsontable.GridSettings> = {
  licenseKey: 'non-commercial-and-evaluation',
  rowHeaders: true,
  colHeaders: true,
  height: 'auto',
  width: 'auto',
  comments: true,
  rowHeights: 15,
  className: 'spreadsheet',
}

It seems like adding the autoRowSize: true property and removing rowHeights solves this problem in my case. But when scrolling a large table (for several tens of thousands of rows), the height of the table seems to change. They should be all the way to the end, but a white empty space appears at the bottom.

const defaultSettings: Partial<Handsontable.GridSettings> = {
  licenseKey: 'non-commercial-and-evaluation',
  rowHeaders: true,
  colHeaders: true,
  height: 'auto',
  width: 'auto',
  comments: true,
  // rowHeights: 15,
  autoRowSize: true,
  className: 'spreadsheet',
}

I tried this demo https://stackblitz.com/edit/dlh5lpmc?file=index.js with the mentioned settings

  licenseKey: 'non-commercial-and-evaluation',
  rowHeaders: true,
  colHeaders: true,
  height: 'auto',
  width: 'auto',
  comments: true,
  rowHeights: 15,
  className: 'spreadsheet',

a a dummy data and I was able to any all of the cells. I tried in using Chrome 135 on macOS Ventura (no zoom or scale enabled).

Could you please specify what spreadsheet class name does?

.spreadsheet {
  color: var(--text-color);
  width: 100%;
  height: 100%;

  & .handsontable {
    font-family: var(--font-family-main), serif;

    tbody,
    thead {
      th.ht__highlight {
        background-color: var(--spreadsheet-bg-color-th-modes-plant);
      }
    }

    th {
      background-color: var(--spreadsheet-bg-color-th);
      .collapsibleIndicator {
        background: var(--spreadsheet-bg-color-th);
        box-shadow: none;
        left: 5px !important;
        right: unset !important;
      }
    }
  }

  & .htCommentCell:after {
    border-top-color: transparent;
  }

  textarea.handsontableInput {
    line-height: 16px;
    font-size: 12px;
  }

  & td,
  & th {
    // you can't do without using rowHeights in the spreadsheets component,
    // because the library does not correctly handle values less than 23px
    // https://handsontable.com/docs/javascript-data-grid/api/options/#rowheights
    height: 16px;
    font-size: 12px;
    border-color: var(--spreadsheet-border-color);

    &.bold {
      font-weight: bold;
    }
  }

  th {
    line-height: 12px;
    align-content: center;
    & .colHeader {
      line-height: 12px;
    }
    & .rowHeader {
      line-height: 12px;
    }
    // other color styles
  }

  td {
    line-height: 16px;
    &.disabledFooter {
      color: #373737;
      background-color: var(--spreadsheet-bg-color-th);
    }
    &.disabledCell {
      color: #373737;
      background-color: var(--spreadsheet-color-disabled);
    }
    &.maxConsumptionHour {
      color: var(--red-color);
    }
    &.minConsumptionHour {
      color: var(--green-color);
    }
    &.manualAdjustments {
      color: var(--blue-light-color);
      font-weight: 700;
      font-style: italic;
    }
    // other color styles
  }

  &Sm {
    & textarea.handsontableInput {
      line-height: 15px;
    }
    td {
      line-height: 15px;
      height: 15px;
    }
    th {
      height: 15px;
    }
  }
}

Thank you for sharing. Within the Stackblitz demo we need to use CSS only so I converted those styles a bit but even with those styles applied (https://stackblitz.com/edit/dlh5lpmc-45qdguk7?file=styles.css,index.js updated demo) I couldn’t replicate the issue.

I am afraid that I would need to ask you to create a demo - it can be in Stackblitz, JSFiddle or any other playground.

Hi. For some reason, there is a problem with the line height on your snippet code. The CSS for th, td specifies a height of 16px, but the actual height is 29px. And if I comment out the rowhight property and add autoRowSize: true, then the row height is not set according to the css values set by it.

new Handsontable(container, {
  data,
  licenseKey: 'non-commercial-and-evaluation',
  rowHeaders: true,
  colHeaders: true,
  height: 'auto',
  width: 'auto',
  comments: true,
  // rowHeights: 15,
  autoRowSize: true,
  className: 'spreadsheet',
});

So how do I set the row height correctly if I want it to be less than the standard 23px? In version 13.1.0, the CSS + rowHights bundle worked, in 15* it no longer works for some reason in my case(

I think I’ve figured out what the cause of this problem is. The autoresize plugin works asynchronously by default. Because of this, a table with a large number of rows does not have time to process with fast scrolling.

Thank you for the update, @mr.fursy

I am happy to hear that the issue is resolved.

ps. yes, rowHeights has to be 23 or more depending on the chosen theme. You can read more about it at https://handsontable.com/docs/javascript-data-grid/api/options/#rowheights