Lazy-ColumnRendering ignores additional HeaderRows
Description
We are currently migrating our old Java Swing Application into Vaadin and are trying to implement Cross Tables using the Vaadin Grid.
The idea is basically something like that:
https://github.com/user-attachments/assets/c9564d70-9e58-4232-8acf-6573da1114bc
The amount of columns depends on whatever items the user wants to see. There can easily be a couple hundred columns shown at once.
During testing, we are currently getting load times of 40-50 seconds for visiting a page having a grid with ~1700 columns.
While some seconds may be cut by changing things on our end, roughly 30-35 seconds are lost by rendering Header Row Cells for columns that are not visible, even though Lazy-ColumnRendering is active.
If we do not create/add HeaderRows, the page with the grid loads much faster.
This is an absolute blocker for us right now, neither getting rid of the Header Rows nor having these load times is really an option.
Expected outcome
Since ColumnRendering.LAZY is active, we expected it to also apply to Header and Footer cells above and below the columns, as these also scroll along.
However, only the first Header and Footer Row seems to follow the Lazy-Rendering (?), all other header cells are rendered eagerly. When recording the performance with Chromium Inspect Element, most time seems to be lost at a slow this.elementsContainer.appendChild(fragment); at IronListAdapter#_createPool() in @vaadin/component-base/src/virtualizer-iron-list-adapter.js, however this is inconsistent, sometimes time seems to be lost somewhere else.
Minimal reproducible example
It's not the 40 seconds we are getting, but I was able to clearly reproduce the performance issues using the following code example:
package com.marcobsidian.vaadin.views.datagrid;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import com.marcobsidian.vaadin.views.MainLayout;
import com.vaadin.flow.component.grid.ColumnRendering;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridVariant;
import com.vaadin.flow.component.grid.HeaderRow;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.provider.AbstractBackEndDataProvider;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.data.renderer.LitRenderer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
public abstract class VaadinColumnRenderingPerformanceView extends VerticalLayout {
@PageTitle("Good Performance")
@Route(value = "fast", layout = MainLayout.class)
public static class FastView extends VaadinColumnRenderingPerformanceView {
protected FastView() {
super(false);
}
}
@PageTitle("Bad Performance")
@Route(value = "slow", layout = MainLayout.class)
public static class SlowView extends VaadinColumnRenderingPerformanceView {
protected SlowView() {
super(true);
}
}
private final Map<String, Map<Integer, String>> cache = new HashMap<>();
private final List<HeaderRow> headerRows = new ArrayList<HeaderRow>();
protected VaadinColumnRenderingPerformanceView(boolean generateHeaders) {
var grid = new Grid<String>();
grid.addThemeVariants(GridVariant.LUMO_COLUMN_BORDERS, GridVariant.LUMO_ROW_STRIPES);
grid.setColumnRendering(ColumnRendering.LAZY);
grid.setHeight("850px");
grid.setWidth("1800px");
grid.setDataProvider(createDataProvider());
var col0 = grid.addColumn(item -> item).setHeader("Col0");
grid.addAttachListener(attach -> {
var col1 = grid.addColumn(item -> item).setHeader("Col1");
var col2 = grid.addColumn(item -> item).setHeader("Col2");
var col3 = grid.addColumn(item -> item).setHeader("Col3");
if (generateHeaders) {
var headerRow1 = grid.prependHeaderRow();
headerRow1.join(col0, col1, col2, col3);
headerRows.add(headerRow1);
var headerRow2 = grid.prependHeaderRow();
headerRows.add(headerRow2);
var headerRow3 = grid.prependHeaderRow();
headerRows.add(headerRow3);
List.of(col0, col1, col2, col3).forEach(col -> col.setFrozen(true));
}
for (int i = 0; i < 2000; i++) {
final int columnNr = i + 1;
var column = grid.addColumn(item -> item + "\\" + columnNr);
column.setHeader("0/" + columnNr);
column.setRenderer(LitRenderer.<String> of("${item.value}") //
.withProperty("value", item -> cache.computeIfAbsent(item, e -> new HashMap<>()) //
.computeIfAbsent(columnNr, col -> item + "/" + col)));
for (var headerRow : headerRows) {
var text = (headerRows.indexOf(headerRow) + 1) + "/" + columnNr;
var span = new Span(text);
span.addClickListener(click -> Notification.show("You clicked: " + text));
headerRow.getCell(column).setComponent(span);
}
}
});
add(grid);
}
private DataProvider<String, Void> createDataProvider() {
return new AbstractBackEndDataProvider<String, Void>() {
@Override
protected Stream<String> fetchFromBackEnd(Query<String, Void> query) {
query.getOffset();
query.getLimit();
query.getPage();
query.getRequestedRangeEnd();
var list = new ArrayList<String>();
for (int i = 0; i < query.getPageSize(); i++) {
list.add("Item " + (i + 1));
}
return list.stream();
}
@Override
protected int sizeInBackEnd(Query<String, Void> query) {
return 1500;
}
};
}
}
MainLayout is just an AppLayout, where I added the Pages as RouterLinks for navigation:
package com.marcobsidian.vaadin.views;
import com.marcobsidian.vaadin.views.datagrid.VaadinColumnRenderingPerformanceView.FastView;
import com.marcobsidian.vaadin.views.datagrid.VaadinColumnRenderingPerformanceView.SlowView;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.html.*;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.RouterLink;
import com.vaadin.flow.theme.lumo.LumoUtility;
public class MainLayout extends AppLayout {
private H1 viewTitle;
public MainLayout() {
setPrimarySection(Section.DRAWER);
addDrawerContent();
addHeaderContent();
}
private void addHeaderContent() {
DrawerToggle toggle = new DrawerToggle();
viewTitle = new H1();
viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
addToNavbar(true, toggle, viewTitle);
}
private void addDrawerContent() {
Span appName = new Span("Vaadin Playground");
appName.addClassNames(LumoUtility.FontWeight.SEMIBOLD, LumoUtility.FontSize.LARGE);
Header header = new Header(appName);
Scroller scroller = new Scroller(createNavigation());
addToDrawer(header, scroller, createFooter());
}
private VerticalLayout createNavigation() {
VerticalLayout nav = new VerticalLayout();
nav.add(new RouterLink("Fast", FastView.class));
nav.add(new RouterLink("Slow", SlowView.class));
return nav;
}
private Footer createFooter() {
Footer layout = new Footer();
return layout;
}
@Override
protected void afterNavigation() {
super.afterNavigation();
viewTitle.setText(getCurrentPageTitle());
}
private String getCurrentPageTitle() {
PageTitle title = getContent().getClass().getAnnotation(PageTitle.class);
return title == null ? "" : title.value();
}
}
Steps to reproduce
-
Visit the "Fast" page -> It takes ~ 3 seconds to render
-
Visit the "Slow" page -> It takes ~ 15 seconds to render
Environment
Vaadin version(s): I was able to reproduce it with Vaadin 24.3.9, 24.5.4 and 24.6.4 OS: Windows
Browsers
Chrome, Edge, Issue is not browser related
I would like to also add the Chrome Inspect Element Performance Recorder JSON-Files, however GitHub seemingly does not allow me to upload these ☹
Another thing that I have not yet fully looked into is that opening the "Slow" page in my example seems to pretty much brick your browser.
The page becomes unusable, selections and scrolling are very slow.
This even affects the browser for one more navigation after leaving the "Slow" page. After that or on Page refresh its fine again.
The current implementation of the Grid does not support this use case and is not intended to be used with this many columns. We can try to research the possibility of optimizing it, but at this point in time, it's not possible to overcome this limitation.
The performace issue should not be limited to many columns, as reducing the amount of columns and instead adding more Header and Footer Rows should have a similar effect. I am going to investigate this further.
The current implementation of the Grid is not intended to be used with this many columns
What's (roughly) the current limit for columns officially supported by the grid?
Hi @yuriy-fix I think my question was missed, could we get an answer? ^^'
Dear @marcobsidian, sorry for the late reply!
There isn’t an official limit on the number of columns that the Grid supports. The Grid will render all header and footer cells regardless of lazy rendering, which only applies to body cells when their corresponding columns are inside the visible viewport.
In other words, even if you reduce the number of columns by adding additional header/footer rows, the browser still needs to render all header/footer cells, and performance will eventually degrade. Ultimately, the practical limit depends on the browser’s ability to manage many DOM elements and the users’ performance expectations.
That said, adding a documentation label to the issue, so the documentation is improved to highlight and explain it.
I hope this helps clarify the situation.