Efficient Large‑Dataset Grids in Vaadin Flow: Server‑Side Paging and Sorting
Learn how Vaadin Flow's Grid uses a DataProvider to fetch only the visible slice of large datasets, enabling server‑side paging and automatic column sorting without blowing up client memory.
23 Sept 2025, 09:17 UTC

The problem: a responsive grid over hundreds of thousands of rows
Loading an entire table into the browser works for a few hundred rows, but once the dataset grows the UI freezes, memory spikes, and the initial request becomes a bottleneck. Vaadin Flow’s Grid solves this by delegating paging and sorting to a DataProvider that fetches only the slice the user actually sees.
Thesis
By implementing DataProvider.fetchFromBackend(Query) you get constant client‑side memory, linear server load proportional to page size, and automatic column sorting without pulling the full result set into the JVM heap.
How the DataProvider drives lazy loading
The Grid calls fetchFromBackend whenever the user scrolls, changes page size, or clicks a column header. The supplied Query object contains:
getOffset()– zero‑based index of the first row to return.getLimit()– maximum number of rows the grid expects.getSortOrders()– list ofSortOrderobjects describing column and direction.getInMemorySorting()– flag you can ignore when you handle sorting in the backend.
Your implementation translates those values into a database LIMIT … OFFSET … (or the equivalent for your persistence layer) and applies ORDER BY clauses derived from the sort orders.
Worked example: a JPA‑backed provider for a Customer entity
@Route("customers")
public class CustomerView extends VerticalLayout {
public CustomerView(CustomerRepository repo) {
Grid<Customer> grid = new Grid<>(Customer.class, false);
grid.addColumn(Customer::getId).setHeader("ID").setSortable(true);
grid.addColumn(Customer::getName).setHeader("Name").setSortable(true);
grid.addColumn(Customer::getEmail).setHeader("Email").setSortable(true);
DataProvider<Customer, Void> provider = DataProvider.fromCallbacks(
query -> {
// 1️⃣ Build a Spring Data Pageable from the Query
Pageable pageable = QueryUtils.toPageable(query);
// 2️⃣ Delegate to repository – only the requested page hits the DB
return repo.findAll(pageable).stream();
},
query -> (int) repo.count() // total count for the scrollbar
);
grid.setDataProvider(provider);
add(grid);
setSizeFull();
}
}
// Helper used by the provider
class QueryUtils {
static Pageable toPageable(Query<Customer, Void> q) {
Sort sort = Sort.unsorted();
for (QuerySortOrder so : q.getSortOrders()) {
Sort.Direction dir = so.getDirection() == SortDirection.ASC ? Sort.Direction.ASC : Sort.Direction.DESC;
sort = sort.and(Sort.by(dir, so.getSorted())); // property name matches entity field
}
return PageRequest.of(q.getOffset() / q.getLimit(), q.getLimit(), sort);
}
}
Where to run: Inside a Vaadin Flow 24 LTS project (Java 17+). The CustomerRepository extends JpaRepository<Customer, Long>. No special permissions beyond normal Spring Boot runtime.
Expected checks:
- Open the view in a browser, scroll down – the Network tab shows a new
GET /customers?offset=…&limit=…&sort=…request for each page. - Click a column header – the request includes
sort=name,asc(ordesc) and the returned JSON reflects the new order. - Monitor the JVM heap (VisualVM or JConsole) while scrolling through 200 k rows; the heap should stay flat because only one page lives in memory at a time.
Trade‑offs and limitations
- Request frequency: Every scroll step fires a backend call. On high‑latency networks the UI can feel sluggish compared with a one‑time full load.
- Offset/limit bugs: An off‑by‑one in
getOffset() / getLimit()produces missing or duplicate rows when the user scrolls quickly. - Total count query: The second callback (
count()) runs on every render. For very large tables consider caching the count or using an estimated total. - In‑memory sorting fallback: If you forget to set
setSortable(true)on a column, Vaadin will sort the already‑fetched page in the browser, giving the illusion of sorting while the backend still returns unsorted data.
Actionable closing
Start by wiring a DataProvider.fromCallbacks implementation against a repository that accepts Pageable. Verify the network traffic and heap profile as described above. Once the baseline works, add a cached total‑count layer or switch to a keyset‑based pagination (seek method) if the offset/limit approach shows latency spikes on deep pages. This pattern scales to millions of rows while keeping the Vaadin Grid snappy and the server footprint predictable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.