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.
Tuned viewport example
Section titled “Tuned viewport example”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>
)
}What each setting does
Section titled “What each setting does”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.
Symptoms and fixes
Section titled “Symptoms and fixes”| Symptom | Fix |
|---|---|
| Rows remount or lose local state after sorting | Add computeRowKey |
| Horizontal scrolling briefly shows empty cells | Raise columnOverscanCount by 1 |
| Vertical scrolling shows blank space around heavy rows | Set a modest increaseViewportBy |
| Typing in filters feels slow | Move filtering into a localModel pipeline or to the backend |
| Cell renderers are expensive | Memoize 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.