Preventing Memory Bloat in Vaadin: Implementing Lazy-Loaded Grids
Stop crashing your server with large datasets. Learn how to implement lazy-loading DataProviders in Vaadin 24 to move data processing from JVM memory to the database.
30 Jul 2026, 10:06 UTC

The Memory Trap of setItems()
When building data-heavy dashboards in Vaadin Flow, the most intuitive way to populate a Grid is using grid.setItems(collection). This approach works perfectly for small datasets, but it creates a scaling bottleneck: it loads the entire dataset into the server's JVM memory for every single user session. If you have 100 users viewing a table of 10,000 records, your heap usage spikes linearly with your user base.
The solution is to shift the burden of data management from the server's memory to the database using a lazy-loading DataProvider. Instead of holding a list, the Grid requests only the specific slice of data currently visible to the user.
How Lazy Loading Works in Vaadin Flow
In Vaadin 24, lazy loading is handled via DataProvider.fromCallbacks(). This method requires two functions: a fetch callback and a count callback.
- Fetch Callback: This is triggered whenever the user scrolls. It provides a
Queryobject containing theoffset(where to start) and thelimit(how many rows to fetch). - Count Callback: This tells the Grid how many total items exist so it can calculate the size of the scrollbar.
By mapping these callbacks to a paginated database query, the JVM only ever holds a few dozen rows at a time, regardless of whether the underlying table has a thousand or a million records.
Implementation with Spring Data JPA
The most efficient way to implement this is by leveraging Spring Data's Pageable and PageRequest. This allows the database to handle the sorting and slicing via LIMIT and OFFSET clauses in SQL.
Example Configuration (Vaadin 24 / Spring Boot 3):
// Assuming a Spring Data JPA Repository: ProductRepository
public class ProductView extends VerticalLayout {
public ProductView(ProductRepository repository) {
Grid<Product> grid = new Grid<>(Product.class);
DataProvider<Product, Void> dataProvider = DataProvider.fromCallbacks(
query -> {
// Convert Vaadin Query to Spring PageRequest
int offset = query.getOffset();
int limit = query.getLimit();
// Map Vaadin sort orders to Spring Sort
Sort sort = Sort.by(query.getSortOrders().stream()
.map(order -> new Sort.Order(Order.Direction.from(order.getDirection()), order.getSorted()))
.toList());
PageRequest pageRequest = PageRequest.of(offset / limit, limit, sort);
return repository.findAll(pageRequest).stream();
},
query -> repository.count()
);
grid.setDataProvider(dataProvider);
add(grid);
}
}
Execution Details
- Where to run: This code resides in your Vaadin View class on the server.
- Permissions: The application must have read access to the underlying database schema.
- Expected Check: Enable SQL logging (e.g.,
spring.jpa.show-sql=true). As you scroll the Grid, you should seeSELECT ... LIMIT ? OFFSET ?queries appearing in the logs, rather than one massive select at startup.
Trade-offs and Performance Constraints
Lazy loading is not a silver bullet; it changes how the Grid interacts with data. You must consider these limitations:
| Feature | ListDataProvider (In-Memory) | Lazy DataProvider (Callbacks) |
|---|---|---|
| Memory Usage | High (scales with data size) | Low (constant per page) |
| Filtering | Instant client-side filtering | Requires backend query updates |
| Sorting | Handled by JVM | Handled by Database (SQL ORDER BY) |
| Count Query | Trivial (list.size()) | Can be expensive on huge tables |
Warning on Count Queries: A common performance trap is the count() callback. On extremely large tables (millions of rows), SELECT COUNT(*) can become slow. If the exact count isn't critical, consider caching the count or using a fixed estimate to prevent the database from scanning the entire table on every refresh.
Managing State Changes
Because the Grid no longer holds the data, it cannot automatically know when a record is updated in the database. If you add or delete a record via a separate form, the Grid will remain stale. You must manually trigger a refresh using:
dataProvider.refreshAll();
This tells the Grid to invalidate its current cache and re-run the fetch callback for the visible area.
Verification Checklist
- Verify that
grid.setItems()is replaced bygrid.setDataProvider(). - Confirm that the
offset / limitlogic correctly calculates the page index for Spring Data. - Check that sorting is passed from the
Queryobject into thePageRequest; otherwise, the Grid will appear to sort visually, but the data returned from the DB will be unordered.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.