Taming Large Datasets in Ant Design Table: Virtual Scrolling and Server-Side Pagination
Rendering thousands of rows in Ant Design Table can freeze the UI. Use the virtual prop for DOM reduction and server-side pagination to keep grids responsive.
16 Nov 2025, 10:43 UTC

The Problem: One Big Array Is Not a Data Grid
When a React page tries to render 10,000 table rows at once, the browser stalls. Ant Design's Table component makes this easy to hit: pass a large array to dataSource and every row becomes a DOM node—an element in the page's document object model that the browser must lay out and paint. The useful takeaway is that you rarely need to send all rows to the client. Pair the built-in pagination prop with the virtual prop, and let the server own the data while the browser owns only the visible slice.
Server-Side Pagination: Let the Backend Do the Heavy Lifting
Instead of fetching everything, request one page at a time. The pagination prop accepts an object with current and pageSize. When the user clicks a page number, Table fires onChange with the new pagination state. You use that event to call your API, typically translating current and pageSize into backend offset and limit parameters. Keep those values in React state so the UI stays in sync with the request.
Ant Design's current page index is 1-based (the first page is 1). Most backend offset values are 0-based (the first record is 0). Compute the offset as (current - 1) * pageSize to avoid an off-by-one error that skips or duplicates the first row.
const [pagination, setPagination] = useState({ current: 1, pageSize: 50 });
const handleChange = (nextPagination) => {
setPagination(nextPagination);
const offset = (nextPagination.current - 1) * nextPagination.pageSize;
fetchPage(offset, nextPagination.pageSize);
};
Run this logic inside a React component file. No special filesystem permissions are needed. The expected check is that clicking page 2 triggers exactly one network request with offset=50 and limit=50. The risk is synchronization drift: if you forget to update pagination.total from the API response, the pager will show the wrong number of pages.
Virtual Scrolling: Render Only What the User Sees
When you must show a long list inside a single page, the virtual prop tells Table to render only the rows visible in the scroll container. The DOM node count drops from the total row count to roughly the number of rows that fit on screen plus a small buffer. This is a rendering optimization, not a data optimization: the full array still lives in memory.
Virtual scrolling relies on predictable row heights and absolute positioning inside the scroll container. Complex CSS overrides that change row height, or features like expandable rows with variable content, can break alignment. Row-spanning logic is especially fragile because the virtual window assumes uniform row boundaries.
Worked Example: Large Pages with virtual and loading
If your API returns large pages—say 1,000 records per request—server-side pagination alone still leaves a heavy DOM. Add virtual to render only the visible subset of that page, and use the loading prop to block interaction while the request is in flight.
import { Table, Tag } from 'antd';
import { useState, useEffect } from 'react';
function LargeDataGrid() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({ current: 1, pageSize: 1000 });
const loadPage = async (current, pageSize) => {
setLoading(true);
const offset = (current - 1) * pageSize;
try {
const response = await fetch(`/api/records?offset=${offset}&limit=${pageSize}`);
const result = await response.json();
setData(result.items);
setPagination(prev => ({ ...prev, total: result.total }));
} finally {
setLoading(false);
}
};
useEffect(() => {
loadPage(pagination.current, pagination.pageSize);
}, []);
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: 'Name', dataIndex: 'name', key: 'name' },
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status) => (
<Tag color={status === 'active' ? 'green' : 'red'}>{status}</Tag>
)
}
];
return (
<Table
columns={columns}
dataSource={data}
loading={loading}
virtual
pagination={{
...pagination,
showSizeChanger: true,
pageSizeOptions: [500, 1000, 2000]
}}
onChange={(nextPagination) => {
setPagination(nextPagination);
loadPage(nextPagination.current, nextPagination.pageSize);
}}
/>
);
}
Placeholders to replace: /api/records, result.items, and result.total. After clicking a page number, open your browser's Network tab and confirm one request fired with the correct offset and limit. Then inspect the DOM: with virtual enabled, the number of <tr> elements inside the table body should be close to the visible row count, not dataSource.length. The main risk is CSS interference—if rows overlap or clip, remove layout overrides on the table row and retest.
Trade-offs and Limitations
| Approach | Best for | Trade-off |
|---|---|---|
| Server-side pagination only | APIs returning small pages (≤100 rows) | Extra round trips; scroll position resets on page change |
| Virtual scrolling only | Static datasets already in memory | Full array in JS memory; breaks with variable row heights |
| Both together | Large pages from API (500+ rows) | More complex state; CSS must respect uniform row heights |
- Virtual scrolling does not reduce memory. The entire page array remains in JavaScript; only the DOM is trimmed.
- Row-spanning and complex cell layouts are fragile. If your grid uses
rowSpanor heavily customized row heights, test virtual mode carefully or avoid it. - Sorting and filtering belong on the server. Set
sorter: trueon a column and handle the sort parameters insideonChangerather than sorting the local array. - Version assumption. Recent versions of Ant Design (v5 and later) expose the
virtualprop; verify against your installed version before relying on it.
Practical Verification
- Run
npm install antdin your project root (the directory containingpackage.json). No elevated permissions are required. Confirmantdappears in the dependencies list. - Render the
TablewithpaginationanddataSource. Open DevTools and verifyonChangefires when you click page numbers. - Toggle the
virtualprop and count the<tr>elements in the table body. Withvirtualon, the count should track the viewport, not the full array. - Watch the Network tab: each page change should produce exactly one API call with the expected
offsetandlimit. - Resize the browser window. If rows misalign after a resize, suspect CSS overrides interfering with virtual scrolling.
Start with server-side pagination for any dataset over a few hundred rows. Add virtual only when page sizes are large or the backend cannot paginate. Test both the DOM row count and the network payload before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.