Skip to content

Performance

Two levers move performance the most: keeping row identity stable across data changes, and controlling how much extra content the table renders around the viewport. Defaults are well-chosen — change them when a specific table shows a specific symptom.

import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader } from '@/components/ui/data-table'
import { localModel } from '@virtuoso.dev/data-table'

const rows = Array.from({ length: 240 }, (_, index) => ({
  id: `SKU-${String(index + 1).padStart(3, '0')}`,
  name: `Product ${index + 1}`,
  region: ['US', 'EU', 'APAC'][index % 3]!,
  status: ['In stock', 'Low stock', 'Backorder'][index % 3]!,
}))

const model = localModel({ data: rows })

export default function App() {
  return (
    <DataTable
      className="rounded-xl"
      columnOverscanCount={1}
      computeRowKey={({ data }) => ('id' in data ? data.id : String(data))}
      model={model}
      increaseViewportBy={120}
      style={{ height: 360 }}
    >
      <DataTableColumn field="name">
        <DataTableColumnHeader>Product</DataTableColumnHeader>
        <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
      <DataTableColumn field="region">
        <DataTableColumnHeader>Region</DataTableColumnHeader>
        <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
      <DataTableColumn field="status">
        <DataTableColumnHeader>Status</DataTableColumnHeader>
        <DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
      </DataTableColumn>
    </DataTable>
  )
}
  • computeRowKey — preserves React row identity when rows move because of sort, filter, group, prepend, append, or replace. See Row Keys for the failure modes.
  • increaseViewportBy — extra vertical pixels rendered before and after the viewport. Raise it when fast scrolling shows blank space or when heavy media should warm up before entering view.
  • columnOverscanCount — extra columns rendered on each side of the horizontal viewport. Raise it when very wide tables feel empty or jumpy during horizontal scrolling.

More overscan means more mounted cells per scroll. Raise it in small steps, table by table.

SymptomFix
Rows remount or lose local state after sortingAdd computeRowKey
Horizontal scrolling briefly shows empty cellsRaise columnOverscanCount by 1
Vertical scrolling shows blank space around heavy rowsSet a modest increaseViewportBy
Typing in filters feels slowMove filtering into a localModel pipeline or to the backend
Cell renderers are expensiveMemoize formatters outside the renderer; avoid creating large objects per cell

To see which rows actually render during a scroll or update, use Inspecting Row Re-renders.