Vaadin Grid Lazy Loading: Moving 100,000 Rows from Browser to Backend
Stop shipping 100,000-row lists to the browser. Learn how Vaadin Grid's DataProvider fetches only visible rows, and what to watch for with offset pagination and cached UI state.
01 Nov 2025, 22:17 UTC

The upfront-loading trap
You wire a Vaadin Grid to a list of 100,000 records, deploy, and watch the browser tab seize. The server is fine; the client is drowning. When you call grid.setItems(list), Vaadin serializes every row to the browser so the component can sort and filter locally. The result is a massive JSON payload, a sluggish DOM, and a memory profile that climbs with every column you add.
The fix is not a bigger server. It is moving the paging, sorting, and filtering work to the backend so the browser only ever sees the rows it can actually display.
How DataProvider moves paging to the server
Vaadin Flow Grid accepts a DataProvider<T, V> instead of a raw list. The Grid asks the provider only for the rows currently visible in its viewport, plus a small buffer. Each request arrives as a Query<T, V> object carrying the offset, limit, sort orders, and any filter value. You return a stream of items and a total count; the Grid handles scroll position, paging, and re-sorting automatically.
This means the browser never holds the full dataset. The server only executes the queries needed for the visible window, which keeps both memory and initial load time proportional to what the user sees.
Worked example: from in-memory list to Spring Data
Start with a Vaadin Flow project. In your workspace directory, run the Maven archetype (requires a standard user account and network access to download dependencies):
mvn -B archetype:generate \
-DarchetypeGroupId=com.vaadin \
-DarchetypeArtifactId=vaadin-archetype-application \
-DarchetypeVersion=<vaadin-version> \
-DgroupId=com.example \
-DartifactId=grid-lazy-demo
Expected check: the command creates a grid-lazy-demo directory with src/main/java. Risk: the first run downloads a large dependency tree.
Next, define a service that translates the Grid query into a backend fetch. For a quick sanity check, use an in-memory list:
public List<Person> fetch(int offset, int limit, List<QuerySortOrder> sortOrders) {
// apply sortOrders to the list if needed
int end = Math.min(offset + limit, people.size());
return people.subList(offset, end);
}
public int count() {
return people.size();
}
Wire it to the Grid:
Grid<Person> grid = new Grid<>(Person.class);
DataProvider<Person, Void> dataProvider = DataProvider.fromCallbacks(
query -> personService.fetch(query.getOffset(), query.getLimit(), query.getSortingOrders()).stream(),
query -> personService.count()
);
grid.setDataProvider(dataProvider);
Open the browser devtools Network tab and scroll. You should see only a handful of requests, each fetching roughly the viewport size rather than the full list.
Now replace the in-memory fetch with a Spring Data JPA repository. The service method becomes:
public List<Person> fetch(int offset, int limit, List<QuerySortOrder> sortOrders) {
Pageable pageable = PageRequest.of(offset / limit, limit);
return personRepository.findAll(pageable).getContent();
}
This assumes the Grid requests whole pages, which is its normal behavior. Enable SQL logging and confirm the generated statements include LIMIT and OFFSET clauses that match the Grid's viewport size. If they do, your database is doing the paging, not your JVM.
Caching, refresh, and stale UI state
Vaadin caches fetched pages by default. Subsequent scrolls reuse cached data unless the underlying source changes. When you mutate data, call refreshAll() on the DataProvider to invalidate the cache and force a fresh fetch.
This caching is a trade-off. It reduces backend chatter, but it also means the Grid can show stale rows if you forget to refresh. You can disable caching entirely by calling setCacheSize(0), but then every scroll triggers a new backend request.
A subtler problem appears when you mix lazy loading with client-side component state. Expanded Grid rows or custom editors bound to old item instances can display stale values after a page refresh. If your Grid uses item details or editors, reset or rebind that state after calling refreshAll().
Limitations to check before you ship
- Offset-based pagination is not free. As the offset grows, the database must scan and discard an increasing number of rows before returning the requested page. If your sort columns lack proper indexes—or if you are querying a NoSQL collection without efficient offset support—performance will degrade as users scroll deeper.
- The total count query required by the Grid can become expensive on large tables. If counting rows takes hundreds of milliseconds, scrolling may stutter while the provider waits for the size callback.
What to do next
Open your application, scroll the Grid, and check the Network tab: requests should be small and infrequent, not one giant payload. Check your database logs for LIMIT/OFFSET matching the viewport. Finally, ensure the columns you sort by are indexed, and reset any expanded-row or editor state after data mutations. If those checks pass, your Grid is doing lazy loading correctly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.