Keep Large Vaadin Grids Responsive with LazyDataProvider
Stop loading entire database tables into your Vaadin Grid. LazyDataProvider fetches only the visible page, keeping large datasets responsive.
04 Mar 2026, 05:16 UTC

The grid that froze on page load
You bind a Vaadin Flow Grid to a database table, start the application, and the browser hangs while the server serializes tens of thousands of rows into the initial response. The grid component is not the problem; eager loading is. By default, a Vaadin grid expects its data provider to supply everything it might need, so large datasets consume server memory and client bandwidth before a single row is visible.
The practical fix is Vaadin Flow’s lazy loading pattern: attach a provider that fetches only the rows for the current page, plus a total row count so the pager renders correctly. Sorting, filtering, and paging remain server-side operations.
How LazyDataProvider changes the flow
A data provider bridges your backend data and the grid. Instead of calling grid.setItems(allProducts), you call grid.setDataProvider(...) with two callbacks:
- Fetch callback: returns a
Streamof items for a specific offset and limit. - Size callback: returns the total number of matching rows, so the grid knows how many pages exist.
Vaadin invokes these callbacks when the user changes pages, sorts a column, or applies a filter. The grid never holds the full result set in memory.
Fetch and size signatures
The callbacks receive a Query<T, F> object. Use query.getOffset() and query.getLimit() for the page window. Use query.getFilters() and query.getSortOrders() if you want to push sorting and filtering to the database instead of doing it in memory. The callback API is stable across Vaadin 14 LTS and current releases, though import paths and minor method names can differ between versions.
Worked example: JPA-backed product grid
Assume a Spring Boot project with Vaadin Flow and a JPA repository. This example loads products 50 at a time.
package com.example.ui;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.provider.Query;
import com.example.backend.Product;
import com.example.backend.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
public class ProductView extends VerticalLayout {
@Autowired
private ProductRepository productRepository;
public ProductView() {
Grid grid = new Grid<>(Product.class);
grid.addColumn(Product::getName).setHeader("Name");
grid.addColumn(Product::getPrice).setHeader("Price");
grid.addColumn(Product::getCreatedAt).setHeader("Created");
grid.setDataProvider(DataProvider.fromCallbacks(
query -> {
int offset = query.getOffset();
int limit = query.getLimit();
List page = productRepository.findPage(offset, limit);
return page.stream();
},
query -> (int) productRepository.count()
));
add(grid);
setSizeFull();
}
}
The repository method might look like this:
public List findPage(int offset, int limit) {
return entityManager.createQuery("select p from Product p", Product.class)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
Where to run: compile and start the application from the project root with ./mvnw spring-boot:run. No elevated permissions are required. Expected check: the application starts on http://localhost:8080. Risk: if port 8080 is already in use, startup fails; configure server.port in application.properties to avoid conflicts.
Trade-offs and when to skip lazy loading
Lazy loading is not free. The size callback must return an accurate total; if it drifts from the actual row count, the pager shows the wrong number of pages or the grid stops requesting data. A slow backend query still causes lag—lazy loading reduces payload size, it does not fix query performance. Consider caching or asynchronous execution if the count query is expensive.
For small tables, skip it. If you have fewer than roughly 1,000 rows, eager loading is simpler and faster because it avoids repeated round trips.
Verify it works
- Open the grid in a browser.
- Open developer tools and select the Network tab.
- Filter for XHR or Fetch requests.
- Click to page 2 or sort a column.
- Confirm the request includes pagination parameters such as offset and limit, and the response contains only the requested page size, not the full dataset.
If the network panel shows one large response on initial load, the provider is still eager. If paging triggers small, targeted requests, lazy loading is working.
LazyDataProvider is a good default for any grid bound to a table that can grow. Start with paging, add server-side sorting and filtering as needed, and keep the size callback consistent with your fetch query.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.