Preventing JVM Heap Exhaustion with Vaadin Grid Lazy Loading
Stop loading entire datasets into JVM memory. Learn how to use Vaadin's CallbackDataProvider to implement server-side pagination and prevent OutOfMemoryErrors in large Grids.
02 Apr 2026, 20:59 UTC

The Memory Wall in Large Data Grids
A common failure point in enterprise Java applications is the "all-at-once" data load. When a developer binds a List<Entity> directly to a Vaadin Grid, the framework loads every single object into the JVM heap. For a few hundred rows, this is negligible. For 100,000 records, it leads to aggressive Garbage Collection (GC) cycles, sluggish UI response, and eventually, OutOfMemoryError.
The solution is to move from a ListDataProvider (in-memory) to a CallbackDataProvider (on-demand). This shifts the burden of data management from the application server's memory to the database's indexing engine, fetching only the slice of data currently visible in the user's browser viewport.
How CallbackDataProvider Decouples UI from Data
The DataProvider API acts as a bridge. Instead of the Grid owning the data, it asks the DataProvider for specific chunks. When a user scrolls, the Grid calculates the required offset (where to start) and limit (how many items to fetch) based on the visible area and a small buffer zone to ensure smooth scrolling.
To implement this, you must provide two pieces of logic to the framework:
- The Count Query: Tells the Grid the total number of items so it can render the scrollbar accurately.
- The Fetch Query: Retrieves the specific subset of data for the current view.
Implementation: Integrating with Spring Data JPA
In a typical Vaadin Flow application (Version 14+), you can integrate a CallbackDataProvider with a Spring Data repository. This allows you to use Pageable requests to handle the pagination at the database level.
// Run this within a Vaadin View class (e.g., @Route("/users"))
// Required permissions: Access to the UserRepository bean
Grid<User> grid = new Grid<>(User.class);
DataProvider<User, Void> dataProvider = DataProvider.fromCallbacks(
query -> {
// 1. The Count Query
// Risk: A slow count query on huge tables can lag the UI
return userRepository.count();
},
query -> {
// 2. The Fetch Query
// Map Vaadin's offset and limit to Spring Data's PageRequest
int offset = query.getOffset();
int limit = query.getLimit();
PageRequest pageRequest = PageRequest.of(offset / limit, limit);
return userRepository.findAll(pageRequest).stream();
}
);
grid.setDataProvider(dataProvider);
Verification and Diagnostics
To verify that lazy loading is functioning, add a System.out.println or a logger inside the fetch callback to print the offset and limit. As you scroll the Grid, you should see these values change dynamically. If you see a single request for the entire dataset, the DataProvider is incorrectly configured as a ListDataProvider.
To check memory stability, monitor the JVM heap using VisualVM or JConsole while scrolling through a dataset of 100,000+ records. The heap usage should remain relatively flat regardless of the total dataset size.
Trade-offs and Performance Bottlenecks
While lazy loading saves memory, it introduces new risks:
- Database Round-trips: Every scroll movement can trigger a network call to the DB. If the database is slow or the network latency is high, the user will see "loading" placeholders in the Grid.
- The Count Penalty: On extremely large tables (millions of rows),
SELECT COUNT(*)can become a bottleneck. In such cases, consider using a fixed-size buffer or an estimated count if the business logic allows. - Sorting and Filtering: When using a custom DataProvider, the Grid's built-in sorting doesn't automatically sort the database. You must intercept the
query.getSortOrders()within the fetch callback and apply those orders to your JPA query.
Summary of Action
If your Grid is bound to a List, you are risking application stability. Replace setItems(list) with setDataProvider(DataProvider.fromCallbacks(...)). Ensure your backend repository supports paginated queries to keep the memory footprint constant regardless of your data growth.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.