When Vaadin Grid Should Go Server‑Side: Lazy Loading vs In‑Memory Data Providers
Large tables in Vaadin Flow can exhaust memory and slow the browser. This post explains how to replace an in‑memory Grid data provider with a server‑side lazy loader backed by Spring Data, highlights trade‑offs, and shows a concrete example.
03 Aug 2026, 23:54 UTC

Large Tables in Vaadin Flow Can Exhaust Resources
When a Grid shows thousands of rows, the default in‑memory data provider pulls the entire collection into the Java heap and then serialises it to the browser. That works fine for a few hundred items, but as the dataset grows you’ll see:
- High server memory consumption.
- Long initial load times.
- Heavy communication over the network for every scroll or resize.
The obvious fix is to let the server fetch only the rows that the client needs – a technique Vaadin calls server‑side lazy loading. The next sections explain how to switch, what you gain, and what you need to watch out for.
In‑Memory Provider – The Default, but Not Scalable
By default, you often write:
grid.setItems(myRepository.findAll());
Vaadin turns the returned Collection into a ListDataProvider. The provider keeps the whole list in memory, and every interaction (scroll, sort, filter) is performed on that list in the browser. The cost is proportional to the number of rows, which quickly becomes problematic for large tables.
Server‑Side Lazy Loading – Pull Only What You Need
Vaadin’s CallbackDataProvider lets you supply a callback that receives a Query object. The query contains:
offset– how many rows to skip.limit– how many rows to return.sortOrders– the current sort configuration.filters– any active filters.
In a Spring‑Boot + Vaadin project you can wire this straight into a Spring Data PagingAndSortingRepository:
grid.setDataProvider(new CallbackDataProvider<>(
query -> {
int offset = query.getOffset();
int limit = query.getLimit();
// Convert Vaadin sort orders to Spring Sort
Sort sort = query.getSortOrders().stream()
.map(order -> new Sort.Order(order.getDirection(), order.getSorted()))
.collect(Sort::by, Sort::and, Sort::and);
Pageable pageable = PageRequest.of(offset / limit, limit, sort);
return repository.findAll(pageable).stream();
},
query -> repository.count() // total number of rows
));
Only the rows needed for the current viewport are requested from the database. The Grid still needs a total count to render the scrollbar correctly, so the second callback supplies that.
Trade‑Offs and Caveats
Database Load
Every scroll, sort, or filter action triggers a new query. For read‑heavy workloads this can increase the load on the database. Mitigate by:
- Using proper indexes for sorted columns.
- Caching the count if the data set is static.
- Throttling rapid user actions with
grid.setDebounceInterval().
Concurrency and UI Thread
Vaadin’s callbacks run on the UI thread by default. A slow query can freeze the browser. Wrap the repository call in a background thread and return a CompletableFuture or use Vaadin’s UI.access() if you need to update other UI components asynchronously.
Stale Data
Because only a page of data is cached, updates to rows outside the current page won’t be reflected until the user scrolls or you explicitly call grid.getDataProvider().refreshAll(). Keep this in mind for real‑time dashboards.
Concrete Example – A Minimal Vaadin 24 App
Assume you have an entity Customer and a Spring Data repository:
@Entity
public class Customer {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
// getters / setters
}
public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long> {}
In your UI class:
@Route("customers")
public class CustomerView extends VerticalLayout {
private final CustomerRepository repo;
private final Grid<Customer> grid = new Grid<>(Customer.class);
public CustomerView(CustomerRepository repo) {
this.repo = repo;
grid.setColumns("id", "name", "email");
grid.setPageSize(50); // rows per page
grid.setDataProvider(new CallbackDataProvider<>(
query -> {
int offset = query.getOffset();
int limit = query.getLimit();
Sort sort = query.getSortOrders().stream()
.map(order -> new Sort.Order(order.getDirection(), order.getSorted()))
.collect(Sort::by, Sort::and, Sort::and);
Pageable pageable = PageRequest.of(offset / limit, limit, sort);
return repo.findAll(pageable).stream();
},
query -> repo.count()
));
add(grid);
}
}
Run the application, open /customers, and scroll. The network tab will show queries like SELECT * FROM customer LIMIT 50 OFFSET 0, then LIMIT 50 OFFSET 50, confirming lazy loading.
Verification Checklist
- SQL Logging – Enable Spring’s
spring.jpa.show-sql=trueand confirm only the requested page is queried when you scroll. - Memory Profiling – Compare the heap usage of an in‑memory Grid (e.g.,
grid.setItems(repo.findAll());) versus the lazy provider. A large dataset should show a noticeable reduction. - Count Accuracy – If your data changes frequently, test that
grid.getDataProvider().refreshAll()updates the scrollbar position correctly.
Actionable Take‑Away
When your Grid starts to show >10,000 rows, switch to a CallbackDataProvider backed by Spring Data. Pay attention to:
- Providing an exact count for the scrollbar.
- Ensuring sort and filter logic lives in the database.
- Running heavy queries off the UI thread.
- Refreshing the Grid after background updates.
With these practices you’ll keep Vaadin’s client‑side performance while scaling the server to handle large datasets.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.