Posted on 26 June, 2023
Welcome back to our blog series on optimizing user interface (UI) performance!
Welcome back to our blog series on optimizing user interface (UI) performance! In our previous post, Streamlining Network Management with a Thoughtfully Designed UI, we explored various techniques for enhancing UI efficiency. Today, we dive deeper into one particular technique that has proven to be highly effective: addressing performance issues through the implementation of visibility observers.
When it comes to user interfaces, one crucial aspect is ensuring that the displayed content is easily accessible and efficiently presented. Within our UI, we have a dynamic dashboard page that empowers users to personalize their experience by adding widgets of their choice. Furthermore, users have the flexibility to create multiple dashboards and effortlessly switch between them, all within the dashboard page. Initially, our expectation was for users to create separate dashboards, each dedicated to monitoring specific sets of items. However, in practice, we noticed a fascinating trend: some users preferred the convenience of adding numerous widgets to a single dashboard.
While this approach offered convenience, it also introduced a challenge. As the number of widgets on a single dashboard increased, users found themselves scrolling through multiple pages to access all the content. Although we didn’t encounter any noticeable performance issues on these widget-packed dashboards, our commitment to continually improving our code based on analysis and user feedback prompted us to seek enhancements for our dashboard and widget implementation.
Let’s delve into how this approach can lead to performance issues. In our initial implementation, we encountered a challenge due to the significant number of operations being performed on widgets that were not visible to the user. Our dashboard comprises various widgets, many of which are equipped with real-time updates. Additionally, we have complex widgets like graphs and maps that require substantial resources for rendering. As a result, our browser was burdened with mounting numerous intricate HTML structures to the DOM continuously. Moreover, it was constantly polling for data for widgets that were not currently visible, resulting in an increased network load.
On the server side, we faced the consequences of these unnecessary requests originating from the browser. Handling these requests led to a significant number of database (DB) calls and unnecessary thread utilization. To provide further context, consider our ICMP monitor graph widgets. When a user selects a one-month range with default options, the server needs to respond with approximately 700 data points to generate an in-depth, high-resolution graph.
This accumulation of operations, from mounting complex HTML structures to continuous data polling and unnecessary server-side requests, posed a threat to the overall performance of our dashboard system. To ensure an optimal user experience, it became imperative for us to find a solution that addressed these issues effectively.
Example ThirdEye dashboard showcasing real-time updated graphs
To address this challenge, we sought a solution that involved unmounting widgets that were not currently visible to the user. However, the question remained: How could we achieve this in React or, more specifically, in JavaScript? Fortunately, in the year 2023, we have the advantage of full support for Intersection Observer in all major browsers. Allow me to share a quote from MDN that highlights the significance of Intersection Observer:
Implementing intersection detection in the past involved event handlers and loop calling methods like Element.getBoundingClientRect() to build up the needed information for every element affected. Since all this code runs on the main thread, even one of these can cause performance problems. When a site is loaded with these tests, things can get downright ugly.
In other words, implementing intersection detection using traditional methods could potentially introduce more performance issues than it resolves. However, with the Intersection Observer API, developers can register a callback function that executes whenever a specified element enters or exits another element or the viewport. It can also trigger when the amount of intersection between the two elements changes by a requested amount. By leveraging this API, websites no longer need to perform these intersection calculations on the main thread, allowing the browser to optimize the management of intersections as it deems appropriate.
Instead of adding Intersection Observer code to every widget component, we devised a more efficient approach. We created a generic wrapper called “VisibilityWrapper” that would automatically unmount the children if they were not visible to the user. Our widget design facilitated this solution, as the polling process would commence upon component mounting and cease upon component unmounting. By implementing the “VisibilityWrapper”, we effectively resolved the issue of unnecessary requests without requiring any additional modifications. This solution proved successful for most of our components, although some presented unique challenges that required alternative approaches.
Implementing a visibility wrapper in React to conditionally render components based on screen visibility.
Now, let’s address a concern related to the graph and map widgets I mentioned earlier. These widgets allow users to temporarily adjust settings to gain a closer examination of the presented data. These settings were locally saved within the widget components. However, you can probably guess what happens when a user scrolls away, causing the component to be unmounted by the Intersection Observer and then reappears. Unfortunately, all the temporarily set settings are lost.
To provide more context, our UI includes settings that can be saved as user preferences, ensuring they are reflected each time the user opens the dashboard. These temporary settings serve as an additional layer on top of the saveable user settings. For example, as previously mentioned, in our graphs, users can set the time range to one month and save it. Consequently, whenever they open the dashboard, it will consistently display the one-month time range. However, if they encounter an abnormal behavior within a specific time range, they can temporarily set a custom time range to scrutinize that specific period.
Returning to our original topic, instead of wrapping the entire component, we could have wrapped only the children or return of that component with the “VisibilityWrapper.” This approach would have resolved the issue of unmounting large HTML structures that are not visible to the user. However, it would not have addressed the problem of excessive requests. For these specific components, we had to utilize the Intersection Observer API internally to achieve the desired behavior.
Another possible solution would have involved lifting the state up and potentially moving it to a global state. However, we opted to stick with using the Intersection Observer API internally. This decision was based on the fact that it required relatively fewer code changes, and both solutions yielded the same benefits in terms of performance and functionality.
One common issue that we didn’t encounter but is worth mentioning is the possibility of layout shifts. When a component is unmounted while it’s not visible and there are parent elements with dynamic size properties, the browser will recalculate the sizes of all elements on the page. This can result in unexpected layout shifts and scroll misbehavior.
The solution to this problem involves measuring the position and dimensions of a complex component when it changes from being visible to not being visible. Then, it should be replaced with a dummy component that has the same hard-coded position and dimensions. However, in our UI, the parent element is the dashboard row component, which has a predefined height that users can adjust and save. As a result, unmounting widgets within that row does not cause any layout shifts or undesired behavior.
By designing the UI in this way and considering the parent element’s fixed height, we were able to avoid layout issues that could have occurred due to unmounting and remounting components.
This blog post delved into the challenge of enhancing UI performance by addressing the issue of managing numerous widgets within a single dashboard. By leveraging the Intersection Observer API, we implemented visibility observers to unmount invisible widgets, reducing unnecessary operations and optimizing network usage. To retain temporary user settings in complex components, we selectively used the Intersection Observer API internally. Furthermore, we discussed the potential problem of layout shifts when unmounting components, but our predefined parent dashboard row component’s height ensured a smooth user experience without disruptive layout changes. Overall, these solutions improved UI performance, streamlined network management, and provided a seamless widget experience
Get hands-on experience with ThirdEye for 30 day free of cost and assess it
by using our evaluation license.