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.

BAD: new array every render
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} />;
}
GOOD: stable reference outside component
// 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.

BAD: new object every render
function MyTable({ data }) {
  return (
    <DataTable
      data={data}
      config={{ pagination: true, pageSize: 25 }}
    />
  );
}
GOOD: useMemo
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.

BAD: data.map inline
function MyTable({ rawData }) {
  return (
    <DataTable
      // Runs on every render of MyTable
      data={rawData.map((row) => ({ ...row, fullName: `${row.first} ${row.last}` }))}
      columns={columns}
    />
  );
}
GOOD: useMemo transformed data
function MyTable({ rawData }) {
  const data = useMemo(
    () => rawData.map((row) => ({ ...row, fullName: `${row.first} ${row.last}` })),
    [rawData]
  );

  return <DataTable data={data} columns={columns} />;
}
The 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.