Data Tables in Web Apps: Pagination, Filters and Virtual Scrolling

Data Tables in Web Apps: Pagination, Filters and Virtual Scrolling — cover image

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.

Why Tables Get Slow

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.

A Real-World Example

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.

Step-by-Step: Building a Fast Data Table

1. Decide what the table is for

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".

2. Move pagination to the server

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.

3. Index for your real filters

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.

4. Avoid N+1 queries

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.

5. Be careful with total counts

"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.

6. Put state in the URL

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.

7. Debounce search and show loading states

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.

8. Use virtual scrolling only when you need it

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.

9. Design bulk actions safely

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.

10. Make exports asynchronous

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.

UX Details That Make Tables Feel Professional

Testing Tables With Realistic Data

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.

Key Benefits of Getting Tables Right

Build a Shared Table Component

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.

Conclusion

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.

Frequently Asked Questions

Should I use offset pagination or cursor pagination?
Offset pagination (page 1, 2, 3) is simple and fine for small or moderately sized tables where users jump between pages. Cursor pagination, which fetches rows after a known key, stays fast on very large or frequently changing datasets and is better for infinite scroll and APIs.
When do I need virtual scrolling?
Virtual scrolling helps when you must render thousands of rows in the browser at once, for example in spreadsheet style editors. It only draws the rows visible on screen. For most business tables, server side pagination with 25 to 100 rows per page is simpler and enough.
Should filtering happen in the browser or on the server?
Client side filtering is fine when the full dataset is small and already loaded. Once tables grow beyond a few thousand rows, or contain data users should not all see, filtering, sorting and searching should happen on the server against indexed columns.
How do I keep table filters when users share a link?
Store filters, sorting and the current page in the URL query string. That makes views shareable, bookmarkable and preserved when users press the back button.
Which library should I use for data tables in React?
Headless libraries such as TanStack Table give you full control over markup and styling while handling sorting, filtering and pagination logic. Paired with a virtualisation library when needed, they cover most business use cases.