Performance Guide
Rendering Strategy
DataTable creates the table instance once via useReactTable, keeping it stable across re-renders. Row models are derived lazily, meaning they are only computed when actually accessed. URL sync runs on mount only — a mountRef guard prevents subsequent re-renders from triggering synchronization again. Feature flags prevent DOM creation entirely: excluded features result in actual JSX exclusion rather thandisplay:none hiding, so no unused nodes are added to the DOM.
Memoization Strategy
Memoization is applied at every layer of the component tree. TableProvider,useResolvedConfig, and useTableConfig all use useMemo to avoid recomputing derived values on every render. Column definitions and index columns inside ClientSideTable are memoized with stable references to prevent TanStack Table from rebuilding internal structures unnecessarily.
Large Dataset Handling
Choose the right rendering strategy based on your dataset size:
- Under 1,000 rows — client-side works perfectly with full sorting, filtering, and pagination in the browser.
- 1,000 – 10,000 rows — consider server-side pagination to keep initial payload sizes reasonable.
- Over 10,000 rows — use full server-side mode with server-side sorting and filtering to avoid loading large datasets into the browser.
Note: virtualization is not currently implemented. All rendered rows are present in the DOM. For very large visible row counts, consider server-side pagination as the primary mitigation.
Anti-Patterns to Avoid
1. Creating columns inside render
Defining column arrays inline inside a component body creates a new array reference on every render, causing TanStack Table to rebuild its internal column model each time.
function MyTable({ data }) {
// This creates a new array on every render
const columns = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
];
return <DataTable data={data} columns={columns} />;
}// Defined once outside the component — stable reference
const columns = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
];
function MyTable({ data }) {
return <DataTable data={data} columns={columns} />;
}2. Passing new config objects inline
Passing an object literal directly as a prop creates a new object reference on every render, even if the values are identical.
function MyTable({ data }) {
return (
<DataTable
data={data}
config={{ pagination: true, pageSize: 25 }}
/>
);
}function MyTable({ data }) {
const config = useMemo(
() => ({ pagination: true, pageSize: 25 }),
[]
);
return <DataTable data={data} config={config} />;
}3. Unnecessary re-renders from parent
Transforming or filtering data inline during render recalculates the entire array on every parent render, even when the source data has not changed.
function MyTable({ rawData }) {
return (
<DataTable
// Runs on every render of MyTable
data={rawData.map((row) => ({ ...row, fullName: `${row.first} ${row.last}` }))}
columns={columns}
/>
);
}function MyTable({ rawData }) {
const data = useMemo(
() => rawData.map((row) => ({ ...row, fullName: `${row.first} ${row.last}` })),
[rawData]
);
return <DataTable data={data} columns={columns} />;
}performance.virtualizationOverscan config option exists and is accepted by the configuration schema. It is reserved for a future virtualization implementation and has no effect currently. Setting it now will allow your configuration to take effect automatically once virtualization support is added.