Scalable rendering in UiPath Apps

a photo of a man working at his computer

UiPath Apps is a low-code application development platform that empowers users to build custom apps designed to interact with automations and other UiPath services. With the freedom to create any type of layout bearing any number of controls on a single page or across multiple pages using the control toolkit, performance remains crucial. Scalability is a top priority, as we aim to achieve constant page load times irrespective of user configurations.

The need for a different rendering technique

Eager rendering and hydration

Originally, all controls on a page were rendered and hydrated up front. So if a page has 1k controls, all these controls will be hydrated and rendered upfront before the user can interact with the page. While this method works well for pages with a low number of controls, as the number of controls increased, the "Time to Interactive" (TTI) grew exponentially, proving unscalable.

Control hydration

  1. Control hydration entails creating a control model containing the business logic of the control. You can consider this VM in an MVVM model.

  2. Hydration involves creating the control model and resolving various properties of the control, e.g. text, tooltip, font size, data source, etc.

  3. On average, a typical control in UiPath Apps has 30 properties that need to be resolved, and only then does the control become usable. Most of these properties are user-configured and can be complex expressions, and can take some time to resolve values.

  4. A typical control can take ~80ms to hydrate. Hence, hydrating all the controls eagerly was not an option for us.

Eager rendering

  1. Eagerly rendering all controls leads to the creation of an extensive DOM, causing the browser to lag on pages containing many controls. To maintain a snappy application, it's essential to minimize the DOM and mitigate longer page load times.

  2. Also, to render everything eagerly, Angular will have to do a lot of work at page load and will increase page load time. Again, this is not a scalable approach, as the rendering time can increase depending on user configuration.

Why not standard viewport-based rendering?

  1. Standard viewport-based rendering relies on knowing the height and width of a control to calculate the number of controls that can fit within the viewport.

  2. UiPath Apps allows users to define the height and width of controls auto, percentage, making it difficult to determine a control's size upfront and define a computation logic for the number of controls that can fit into the viewport.

Solution: lazy rendering and hydration of controls

a graphic of an eager rendering and hydration and a lazy rendering and hydration

At this point, we were sure that eager rendering and hydration of controls was not an option for us because we wanted the load time to be constant irrespective of the number of controls on the page. In the diagram above, we can see on the left that all the controls are hydrated and rendered eagerly irrespective of the viewport size. While on the right, only the controls that can fit into the viewport are hydrated and rendered.

To achieve this, we used a Javascript API Intersection Observer that allows us to know if an element on the page is visible on the screen or not. Before we delve into the approach, we should understand a few things:

Panel control

UiPath Apps has a panel-control (aka container) that is used to render controls. These panels can be nested to any level to create various layouts. The panels can have a horizontal or vertical orientation.

Visibility watcher

We added an empty div in the panel control called Visibility Watcher. An intersection observer is attached to the visibility watcher element, telling us whether the visibility watcher is visible (intersecting) on the viewport or not. A visibility watcher and a corresponding intersection observer are present in each panel control. Depending on the orientation of the panel, the visibility watcher acquires different widths and heights. This is done deliberately so that even if a single pixel of the visibility watcher intersects with the viewport, we consider it visible on the screen.

  • For a horizontal-oriented panel: width = 1px, height = height of the panel

  • For a vertical-oriented panel: width = width of the panel, height = 1px

Approach

a graphic of the app rendering process
Lazy rendering
  1. The application's rendering starts at the root panel, where it starts its intersection observer. Since the panel has no controls rendered yet, the visibility watcher of the root panel will intersect with the viewport.

  2. If the visibility watcher is intersecting with the viewport, the panel will hydrate and render five controls (first batch). Upon rendering a batch, the visibility watcher may remain in the viewport or go out of the viewport.

  3. After rendering a batch of controls, the intersection observer of the panel is restarted. When re-started, it will again give a callback whether the visibility watcher is intersecting with the viewport or not. If it's intersecting, we render the next batch of controls, and this process keeps ongoing until the visibility watcher goes out of view (not intersecting).

  4. Each panel is concerned with the rendering and hydration of its immediate children. The hydration and rendering of nested levels are handled by the panel for that level.

  5. If there are a large number of controls in a panel and when the user scrolls (left/down), the visibility watcher will again start intersecting with the viewport, and the next batch of control is hydrated and rendered.

  6. The visibility watcher and intersection observer remain live until all the controls in a panel are rendered.

  7. When a panel at a sub-level is rendered, it doesn't immediately render its children. It follows the same approach as the root panel and only renders depending on the intersection of the visibility watcher.

This way, we never render the controls eagerly and always try to render as little as possible to fill the viewport. At times, if the child controls are huge in size, we may render more than what can fit in the viewport due to our batch size of 5. But this is a conscious design choice we made to not over-engineer the solution.

Lazy hydration
  1. When the panel has determined the batch of control IDs to render, it asks its model to hydrate those controls.

  2. The hydration logic of the controls is centralized in the page model. The panel model then asks the page model to hydrate its controls and give it back to the view layer via the panel model.

  3. The hydration process of control happens in two phases: the creation of a model and property resolution.

  4. The creation of a model is a synchronous and pretty fast operation. The rendering is resumed as soon as the control model is created. The model is then attached to the angular component of the control.

  5. The property resolution being a costly operation, is triggered asynchronously and doesn't block the rendering; once the property resolution is completed, the control aligns itself with the property values from the model.

  6. The page model maintains a cache of all the control models against their IDs, so in case a hydration request for the same control comes, it can reuse the same control model.

Challenges

We faced several challenges to keep some of the existing flows working. The challenges arise due to lazy hydration of controls as opposed to the eager hydration of controls before the enhancements. Due to lazy hydration, a control will not be upfront hydrated, which means its model doesn't exist in the application.

Random access to controls

UiPath Apps allow referencing a control at any depth level in the control hierarchy in the bindings or expressions to read/write a property of a control. Due to lazy hydration, control may not be hydrated by the time it is referenced in a binding to read or write its value.

 -
  1. In the picture above, Button 1 is trying to set the value of Textbox 1 and the text box is present outside the viewport, which means it will not be hydrated. If the control is not hydrated, the operation to set the value of the textbox will fail.

  2. To overcome this challenge, we built a concept of forceful hydration of control when demanded.

  3. The binding infrastructure of the UiPath Apps asks the Page model to provide the control model by passing the control ID. If the page model has the control model in its cache, then it returns the same model; otherwise, it creates the control model, hydrates it, and then returns it to the binding infra.

  4. Only that specific control (Textbox 1) will be hydrated, and not its parent, i.e. Sub-panel.

  5. Hydration doesn't mean that the forcefully created control will be rendered, either. It will only render when the sub-panel of the control decides to render it.

  6. Once a control is hydrated/rendered, the UiPath Apps will not destroy the control model even if the control goes out of the viewport. This is a conscious design choice taken for a smooth user experience while scrolling back to a previously visited part of the page.

  7. The control and page model are destroyed once the user navigates away from a page and are re-created if the user comes back to the page again.

Parent-child relationship of control and its panel

The control model in UiPath Apps maintains a relationship with its parent. The control needs to know about its panel model (parent) to work in certain flows. As explained in the section above, a control can be forcefully hydrated at any time without its panel being hydrated. So we had to break this dependency to facilitate the forceful hydration of control. To overcome this, we built a concept of connected and disconnected controls.

  1. Connected control: a control is connected if it has the instance of its panel model available to it. When a control is connected, it can fulfill all its capabilities.

  2. Disconnected control: a control is disconnected if it doesn't have the instance of its panel model available to it. A disconnected control can only work with limited capabilities.

  3. A task-deferring mechanism was built in the control model layer, where control can defer an operation if it's disconnected. Those operations are put in a queue to be processed later.

  4. When a panel tries to hydrate and render a control, the page model is responsible for hydrating the control. If the control is already hydrated, it connects that control with its panel, as it knows its parent at this point.

  5. As soon as the control is connected, the deferred task queue is drained, and all the deferred tasks are performed.

In the case of a forceful hydration scenario, the page model hydrates the control model but keeps it disconnected since it is not aware of its parent at that point.

 -

Let's talk numbers

We measure the load time of UiPath Apps in terms of LCP. Below is a comparison of load time before and after these enhancements coupled with a few others.

-

Challenges yet to be solved

We have been able to significantly reduce the load time of UiPath Apps runtime, but there are lots of areas that need further improvements and innovation.

Expression re-evaluation

  1. Re-evaluation of an expression can be a costly operation because they are user-configured. Expressions can be as simple as a substring to a very complex LINQ operation.

  2. Whenever a dependency of an expression is changed, it's re-evaluated.

  3. A typical control can have 15+ user-configured expression-able properties.

  4. Multiple expressions on multiple controls can be dependent on a variable value. Whenever the value of the variable changes, all these expressions need to be re-evaluated. In large Apps, we have seen performance issues where evaluation can take a lot of time and cause slowness in user experience.

Eagerly loaded code

  1. UiPath Apps has a plethora of controls and integration with other UiPath services. Each of these features requires new JS code to be added to the application.

  2. Every App doesn't use all the controls and all the features of UiPath Apps, but still, all the JS code is downloaded on the user's machine.

  3. This delays the cold load of the application and degrades the user experience.

App validation in UiPath Apps Studio

  1. UiPath Apps Studio is the application that app developers use to build apps.

  2. Studio aggressively validates the bindings as the user is changing the app and provides feedback to the user about invalid bindings in real time.

  3. In large-size Apps, validation becomes a bottleneck, causing lags and a degraded user experience. We have tons of other exciting challenges that keep us motivated and challenged every day and encourage us to think out of the box.

Conclusion

The introduction of lazy rendering and hydration techniques has significantly enhanced the scalability and performance of UiPath Apps. With the use of Intersection Observers, we can now ensure an optimal user experience regardless of the number of controls on a page. As a result, application developers can create seamless, versatile layouts without compromising on performance—cementing UiPath Apps as a leading low-code application development platform.

a portrait photo of Suhail Siddiqi
Suhail Ahmad Siddiqi

Principal Engineering Manager, UiPath