A couple of weeks ago I was coding a demo app when it dawned on me that if I was to use my code in a real-world application, it would consume unacceptable amounts of memory once it reached 100 or more concurrent users. Vaadin Flow makes it easy to start coding web applications, even if you have no experience with web development. The fact that it encapsulates the underlying request-response paradigm that is present in every web application, also makes it easy to forget that you are coding a web application and that there is a server sharing limited computing resources between concurrent users.
In this article, I discuss some of the most common mistakes we make as developers when using Vaadin that have a high potential to degrade the performance of an application:
- Eagerly building components that might never be displayed
- Using collections or arrays instead of callbacks
- Hiding, instead of removing, auto-generated Grid columns
- Abusing component renderers in Grid
- Running complex data operations in the "UI thread"
I also discuss ways to avoid these pitfalls, so worry not—I've got your back.
Pitfall 1: Eagerly building components that might never be displayed
If you have a view with multiple tabs, dialogs, or layouts that you show when the user performs an action on the UI, build the content of those tabs, dialogs, or layouts lazily, instead of building them in the constructor, for example.
Take a look at this view in a hypothetical e-commerce application:
@Route
public class EagerView extends VerticalLayout {
public EagerView(Database database) {
// build dialog with orders
Grid<Order> grid = new Grid<>();
grid.setItems(database.getOrders());
Dialog dialog = new Dialog(
grid
// ... more dialog components here ...
);
add(
// a button to open the orders dialog
new Button("See orders", event -> dialog.open()),
// ... more view components here ...
);
}
}
This view includes a button to show a dialog with orders. The data is gathered from a database. The problem here is that the user might never click the button to show this data, but the application loads the data and builds the UI nevertheless—a waste of computing resources if the user leaves the view without clicking the button. A better solution is to build the dialog only when the user clicks the button:
public LazyView(Database database) {
add( // to the view
// a button to open the orders dialog
new Button("See orders", event -> {
// show a dialog with orders on button click
Grid<Order> grid = new Grid<>(Order.class);
grid.setItems(database.getOrders());
new Dialog(
grid
// ... more dialog components here ...
).open();
})
// ... more view components here ...
);
}
Pitfall 2: Using collections or arrays instead of callbacks
If you have a component that shows items and the item list can be expected to grow with time, always use callback methods with lazy loading, instead of setting items directly as collections or arrays using the setItems method. Vaadin components, such as Grid and ComboBox, include lazy-loading features at two levels. First, the framework sends data (items) to the client using pagination, reducing the traffic in the network. As the user scrolls through the rows or items in a component, the framework sends pages of data to the client. Second, the framework includes APIs that allow you to hook in your own lazy loading mechanisms to reduce traffic between your view code and backend services.
Take a look at this snippet of code:
Grid<Order> grid = new Grid<>();
grid.setItems(database.getAllOrders()); // it can be a lot of orders
This code builds a Grid with all the orders in the database. If this is part of an e-commerce application, it's very likely that the number of orders will grow with time, as it is the number of concurrent users of the application (assuming the business becomes a success!). With more concurrent users and more orders in the database, the server will need more and more memory to store a list of orders for each user. A solution for this is to use a data provider instead:
// delegates data reading to a data provider with lazy loading capability
grid.setDataProvider(new OrdersDataProvider(database));
The OrdersDataProvider class must implement the DataProvider interface and its 6 methods directly or indirectly through the implementations provided by Vaadin. Moreover, setDataProvider method is deprecated in latest versions of Vaadin. A better alternative is to use callback methods:
grid.setDataProvider(DataProvider.fromCallbacks(
// fetch callback
query -> {
int offset = query.getOffset();
int limit = query.getLimit();
Stream<Order> orders = database.getOrders(offset, limit);
return orders;
},
// count callback
query -> {
int count = database.countOrders();
return count;
}
));
In the case of the Grid component, the count callback is optional.
Pitfall 3: Hiding instead of removing auto-generated Grid columns
If you have a Grid with auto-generated columns and you don't want to show some of them, don't hide them, rather remove them. When you hide a column, potentially complex getters and renderers could be executed without need. Again, a waste of computing resources.
Continuing with the e-commerce example, suppose the Order class includes a getter called getComplexData() that uses a lot of memory and processing power. You don't want to show that "complex data" in a Grid, but you still want to take advantage of the auto-generation of columns feature. You can let Vaadin generate the columns for you and then hide the column you don't want to show:
// grid with auto-generated columns
Grid<Order> grid = new Grid<>(Order.class);
grid.getColumnByKey("complexData").setVisible(false);
When the Grid is rendered, the getComplexData()method is invoked for each item regardless. Moreover, depending on the version of Vaadin, the columns might still be present in the DOM in the browser. To solve this problem, remove the column instead of hiding it:
Grid.Column<Order> toRemove = grid.getColumnByKey("complexData");
grid.removeColumn(toRemove);
Pitfall 4: Abusing component renderers in Grid
If you have a Grid with many columns, avoid using component renderers. A component renderer is used when you call the addComponentColumn method or the ComponentRenderer class when you add columns to a Grid.
Take a look at this example:
Grid<Order> grid = new Grid<>();
grid.addComponentColumn(order -> {
// creates one button per row. It can be a lot of buttons
Button button = new Button("Edit");
button.addClickListener(event -> edit(order));
return button;
});
For each cell in a column that has a component renderer, Vaadin creates the server-side component. This can get out of control as you have more rows and columns with component renderers.
Typically, component renderers are used to allow data editing or to invoke actions on an item. To avoid rendering too many components, you can redesign the application to show pop-up dialogs or panels for editing data in a form, instead of in the Grid. You can also consider moving action buttons from the Grid to a toolbar (this is how I implemented it in the CrudUI add-on). When you have strict requirements for the UI and need to place components in Grid cells, use template renderers instead:
grid.addColumn(TemplateRenderer.<Order>of(
// a button with no server-side counterpart
"<vaadin-button on-click='editOrder'>Edit</vaadin-button>"
).withEventHandler("editOrder", order -> edit(order))); // calls the server
Pitfall 5: Running complex data operations in the "UI thread"
If you have to show data that requires operations that might take time because of complexity in the calculations or dependence on external systems, use background threads to run these operations, instead of executing them directly in, say, a click listener.
Suppose that placing a new order requires consuming an external web service and calculating numbers that involve the execution of logic that takes some time:
Button button = new Button("Place order");
button.addClickListener(event -> {
// calls a potentially slow service
String result = externalService.placeOrder(order);
Notification.show(result);
});
The call to the external service might take more time than expected. Even if it isn't so long that users get impatient and, for example, reloads the page, when the number of concurrent users starts to grow in the application, the computing resources will become insufficient, as more "UI threads" will be busy waiting for the external service to run. A better implementation would take into account the potential volume of users invoking the service and provide a pool of threads or a queue to process all the requests. In these cases, server push is useful to update the UI once the task is completed. For example:
@Push
@Route
public class ThreadPoolView extends VerticalLayout {
private static ThreadPoolExecutor executor =
(ThreadPoolExecutor) Executors.newFixedThreadPool(10);
public ThreadPoolView(ExternalService externalService) {
Order order = new Order();
Button button = new Button("Place order");
UI ui = UI.getCurrent();
button.addClickListener(event -> {
// the task is run in a new thread
executor.submit(() -> {
String result = externalService.placeOrder(order);
// uses Push to update the UI
ui.access(() -> Notification.show(result));
});
});
add(button);
}
}
Conclusion
I have tried to briefly explore some of the mistakes that could lead to performance issues in Vaadin applications and provide a “light” to help you solve them in your own applications. Always remember to keep in mind that your code is potentially going to be executed on a single machine (the server), but in multiple threads and serving multiple users at the same time. Pay attention to the data that you include in your class fields and UI components. Try to keep the components tree as flat as possible and always remember to use lazy loading at the data and UI level, creating components only when they are going to be shown.