Almost every business web app ends up being a set of tables. Orders, customers, invoices, leads, tickets, inventory. Users spend most of their working day scanning, filtering and acting on rows of data. Yet data tables are often built in a hurry during the first release and then left alone until the day a customer complains that the orders page takes ten seconds to load.
This guide covers how to design and build data tables that stay fast and useful as your data grows: pagination strategies, filtering and sorting, virtual scrolling, bulk actions, exports and the UX details that make a table feel professional.
A table that worked perfectly with 500 rows in staging can struggle in production for a few predictable reasons:
The fix is rarely one magic library. It is a set of sensible defaults applied consistently across the app.
Imagine a B2B SaaS product for logistics companies where the main screen is a shipments table. Early customers have a few hundred shipments a month. A year later, a larger customer arrives with hundreds of thousands of historical shipments and dozens of dispatchers using the screen all day.
The original implementation fetched all shipments for the account and sorted them in the browser. For the new customer, the initial load could easily take many seconds and the tab could freeze on older laptops. A typical rebuild for a situation like this would move filtering, sorting and pagination to the server, add composite indexes for the most common filter combinations (account, status, date), switch the default view to "last 30 days" and replace the exact total with an estimated count for very large result sets. Changes like these could plausibly bring page loads back to well under a second without changing the database or the hosting plan. The figures are illustrative, but the pattern repeats in almost every growing product.
Before writing code, ask what users actually do on this screen. Are they scanning for exceptions, searching for a specific record, or processing a queue one by one? The answer decides the default sort, the default filters and which columns matter. A dispatcher wants "delayed shipments first", not "newest created first".
For any table that can grow, the API should accept page or cursor parameters and return only one page of rows. Choose your strategy deliberately:
Many products use offset pagination for admin tables and cursor pagination for feeds and APIs. Both are valid; the mistake is not choosing at all.
Look at which filters and sort orders people actually use and index for them. A composite index on (account_id, status, created_at) can serve the most common "my account, this status, newest first" query directly. Use your database's query plan output to confirm the index is used. Our guide to fixing slow Postgres queries with indexes walks through this process step by step.
If each row shows the customer name, assigned user and last status change, load those in a single joined query or batched lookups, not one query per row. Most ORMs offer eager loading for exactly this case.
"Showing 1 to 50 of 1,284,903" looks nice but can be expensive. Options include caching counts, using database estimates for large results ("about 1.2 million"), or showing "Next" without an exact total. Users rarely need the exact number on a table of a million rows.
Filters, sort order, search text and page number belong in the query string. That makes views shareable ("look at this list"), bookmarkable, and preserved on refresh and back navigation. It also makes debugging customer reports much easier, because they can simply send you the link.
Do not fire a request on every keystroke. Wait a few hundred milliseconds after the user stops typing. Keep the previous results visible with a subtle loading indicator rather than flashing an empty table, and cancel in flight requests when a new one starts so old results never overwrite new ones.
Virtual scrolling renders only the rows in the viewport, which lets the browser handle tens of thousands of rows smoothly. It is the right tool for spreadsheet style editors, log viewers and large client side datasets. For typical paginated business tables it adds complexity (variable row heights, keyboard navigation, accessibility, sticky headers) without much benefit. Start with pagination and reach for virtualisation when a specific screen demands it.
Users love selecting many rows and acting at once: assign, export, archive. Make the scope explicit. "Select all 50 on this page" is different from "select all 12,400 matching this filter". For large bulk actions, run the work as a background job, show progress, and make the action reversible where possible.
Exporting a filtered view to CSV is one of the most requested features. Generating a large file inside a single web request risks timeouts. Queue the export, stream rows from the database in batches, store the file and notify the user when it is ready. The same care you put into imports, covered in our guide to CSV data import for SaaS onboarding, applies in the other direction too.
Many table problems are invisible in development because the local database holds a few dozen rows. Seed a staging environment with a realistic volume of data, ideally similar in shape to your largest customer, and test the slowest filter combinations there. Check the deepest pages, the widest date ranges and searches that return nothing. Measure both the API response time and the time the browser takes to render the result. Add a simple performance check to your review process so that a new column with an expensive join or a missing index is caught before it reaches users, not after a support ticket.
Instead of each developer building tables differently, create one shared component that standardises pagination, sorting, filters in the URL, loading states, empty states and column settings. Headless libraries such as TanStack Table handle the logic while you keep full control over design. Pair it with a matching API convention so every list endpoint accepts the same parameters. This is one of the highest leverage pieces of a SaaS product's frontend foundation.
Treat your table component like a product inside your product. Most of your users will spend most of their time inside it.
Fast, usable data tables are not glamorous, but they shape how your product feels every single day. Move pagination, filtering and sorting to the server, index for real usage, keep state in the URL, handle bulk actions and exports in the background, and reserve virtual scrolling for screens that genuinely need it. If you are planning a dashboard heavy product, our web development team builds these patterns in from the first release so you do not have to retrofit them later.