Erik Lumme.dev

The Vaadin UI class: what is it, where do UIs come from, and where do they go?

Posted 22 Dec 2021 by Erik Lumme

The UI class is central to Vaadin applications, as it holds the state of all active server-side components. This article explains what the UI is, but also when it is instantiated, and when it's discarded.

The UI is one of the most central classes of a Vaadin app. It is a Component whose underlying HTML element is the body element. We can interact with the underlying element through the Element API, for example UI.getCurrent().getElement().setAttribute("foo", "bar"). As there can only be one body element per HTML document, and there is generally one document per browser tab, a UI can also be seen as corresponding to a browser tab.

Through the UIInternals class, a UI instance stores all attached server-side components in a StateTree. It also stores the PushConnection when push is used, any pending JavaScript invocations, the current theme, and more. Because of this, it is good to be aware of the UI’s lifecycle, so that UI instances do not hang around for longer than necessary.

Note: This article was written around Vaadin 14.8.0 using Java views only. The boostrapping for TypeScript-based projects works a bit differently.

When and how is a UI created?

When the VaadinServlet receives a request, the request is forwarded to a VaadinService instance. This in turn offers the request to each of its request handlers one by one, until a handler is found that can handle the request. The last handler to be invoked is the BootstrapHandler, which is responsible for creating a new UI instance when no other handler can handle the request.

The primary handler is the UidlRequestHandler. When a user interacts with a Vaadin application, such as when clicking a button, an RPC is generally made to the server. The server runs the appropriate event handlers and returns the resulting UI updates, such as to show a notification. The RPC and UI updates are contained in a UIDL request (user interface definition language). There is also a HeartbeatHandler for heartbeats, more on those later.

The BootstrapHandler looks for the UI class name from the deployment configuration (com.vaadin.flow.component.UI by default) and instantiates the class. At this point, the locale is set, and the push configuration is read from the top-most parent of the current navigation target.

The current UI instance is then made available to UI#getCurrent before UI#init is called. The VaadinSession holds a list of all UIs in the session, and uses an incrementing counter to assign a ID to each UI that is unique for that session. This ID is included in UIDL and heartbeat requests, so that the server can differentiate between the UIs of a session.

Some UIDL requests and a heartbeat request as seen from the Chrome devtools, all with an UI ID of 1.
Some UIDL requests and a heartbeat request as seen from the Chrome devtools, all with an UI ID of 1.

After this, any UI initialization listeners are called, and Router#initializeUI is called to perform the initial navigation. This is where the content is added to the server-side UI state tree. A BootstrapPageBuilder is used to write the HTML of the bootstrap page to the response. The response includes the initial structure of the UI, as written by UidlWriter.

Part of the boostrap HTML, including the initial UIDL in the form of syncId, clientId, constants, and changes.
Part of the boostrap HTML, including the initial UIDL in the form of syncId, clientId, constants, and changes.

When and how is a UI destroyed?

We mentioned previously that a UI can be seen as corresponding to a HTML document or a browser tab. When refreshing a tab, closing a tab, or navigating through the browser, the underlying HTML document will be unloaded, and the corresponding UI will eventually be closed and detached.

Note that when navigating using a RouterLink, an anchor with the router-link attribute, programmatically using the Router, or using the back/forward buttons after one of the former, the navigation will be performed by Vaadin without a page reload, and the current UI will be retained.

There are three ways for the UI to be closed and detached: session destroyed, missed heartbeats, and immediate or manual closing. We will cover these next.

Session destroyed

A Vaadin session can be programmatically destroyed, for example when a user logs out. The Vaadin session is stored as an attribute in the HTTP session, and when the HTTP session expires, the VaadinSession#valueUnbound method will be called, which also destroyes the VaadinSession.

In either case, the VaadinService loops through all UIs in the session and closes them, which marks them for removal. Next, the reference to the VaadinSession will be removed from each UI, and vice versa. Removing the session reference from the UI also causes the onDetach methods of the UI and all contained components to be called. If no other references to the UI remain, the UI can now be garbage collected.

The expiry check of the HTTP session is typically performed at fixed intervals by a background thread in the servlet container. Tomcat 9 uses a thread called Catalina-utility-{number}, and the check is performed once a minute. This means that it might take up to a minute longer than the configured session timeout for a session to expire.

Heartbeats

A user might want to keep a tab open for a long time without interacting with it, for example to monitor some real-time values. In this case the session should not expire. This is achieved by heartbeat requests that are sent from the client to the server at regular intervals. As long as this interval is shorter than the session timeout, every heartbeat will refresh the session timeout in the servlet container, and the session won’t expire.

If heartbeats shouldn’t prevent the session from expiring, the closeIdleSessions deployment configuration parameter can be set to true. When set, Vaadin will record the timestamp of the last UIDL request, and invalidate the session when only heartbeats have been received for the period configured in the session timeout.

As a user closes, opens, and refreshes tabs, new UIs will be created. As long as the user actively uses the application, the session will not expire. As such, Vaadin doesn’t only rely on session expiration for closing and detaching UIs.

Instead, the heartbeats are also used to detect dead UIs. Each time the VaadinService finishes handling a request, it calls VaadinService#closeInactiveUIs. This will loop through all UIs in the session, and compare the timestamp of the UI’s last heartbeat (stored in the UI internals) with the heartbeat interval. If that UI has missed three heartbeats, i.e. it has not sent a heartbeat for a duration greater than three times the heartbeat interval, then UI#close will be called, and the UI will be removed from the session. As this check is only performed at the end of requests in the same session, it is only useful when there is still at least one active UI in the session.

Immediate or manual closing

In case you want to manually close a UI because you know it is no longer active, it is enough to call UI#close. At the end of the current request, or the next request within this session if done from a background thread, the UI will be detached and references cleared.

When using @PreserveOnRefresh, a new UI will still be created upon refreshing, but the old one will be closed immediately.

A possible approach to detect when a browser tab is closed or refreshed is to use the browser beforeunload event to send a request to the server. The downside of this is that if the request is asynchronous, the browser does not guarantee that it will be completed. If it is synchronous, it might prevent the tab from closing or refreshing. The Beacon API solves this issue, and is supported in all modern browsers. At the time of writing, there is a feature request to add support for the Beacon API in Vaadin Flow, such that UIs would be immediately detached.

The feature is not yet implemented, and even if it was, it’s not a catch-all solution. Events such as browser crashes, power outages, and network interruptions would all prevent a request from being sent. In this case, session expiration is the fallback.