Optimizing UI Performance in Unity: Deep Dive into LayoutElement and LayoutGroup Components

magic Hung
6 min readSep 16, 2023

--

Image via https://www.flickr.com/photos/juhansonin/

While Unity’s UIToolKit is not yet stable and mature, the UGUI system continues to be an essential tool for developers. As we await the full potential of newer technologies, mastering UGUI and optimizing its performance becomes crucial. This article dives into the core aspects of UGUI’s auto layout, placing special emphasis on the LayoutElement and LayoutGroup components.

RectTransform’s anchors and offsets offer a robust capability for building responsive UI, however they have limitations when it comes to supporting content-adaptive responsiveness, where components dynamically adjust their size based on varying content, such as multilingual text or components with multiple states.

On the other hand, Unity’s auto-layout feature, LayoutElement, presents its own challenges. Its performance issues are well-known among developers, so much so that Unity’s official optimization guidelines advise limiting its use, noting, “Avoid Layout Groups where possible. Problem: Every UI element that attempts to modify its Layout will perform at least one GetComponents call.”

Despite its challenges, the LayoutElement system remains an invaluable and potent tool for layout design. To truly harness its potential and create an optimized solution, it’s essential to delve deep into its workings and craft a version that aligns with our unique requirements.

The LayoutElement system operates through a combination of LayoutElement, LayoutGroup, LayoutRebuilder, UIBehaviour, and CanvasUpdateRegistry :

UIBehaviour:

  • UIBehaviour is the base class for all UI components in Unity. It provides a set of common methods that are invoked at various points in the lifecycle of a UI component, such as OnRectTransformDimensionsChange, OnBeforeTransformParentChanged, OnTransformParentChanged, OnDidApplyAnimationProperties, OnCanvasGroupChanged, OnCanvasHierarchyChanged, etc.
  • Both LayoutElement and LayoutGroup are derived from UIBehaviour, which means they inherit these lifecycle methods and can override them to provide specific behaviors.

LayoutElement:

  • Represents an individual UI element that can participate in the layout system.
  • It provides properties like preferredWidth, preferredHeight, flexibleWidth, etc., which are used by the layout system to determine how this element should be sized and positioned.
  • When properties of a LayoutElement change, it marks itself as "dirty", signaling that its layout needs to be recalculated.

LayoutGroup:

  • Represents a container of UI elements and defines rules for laying out its children.
  • Examples include HorizontalLayoutGroup, VerticalLayoutGroup, and GridLayoutGroup.
  • When a child LayoutElement is marked as dirty, or when properties of the LayoutGroup itself change, the group recalculates the layout of its children based on its rules.

ICanvasElement: This interface defines the basic structure of an element that can exist on a Canvas. It has methods like:

  • Rebuild(CanvasUpdate executing): Rebuild the element for the given stage.
  • LayoutComplete(): Callback sent when the element has completed layout.
  • GraphicUpdateComplete(): Callback sent when the element has completed a graphic rebuild.
  • IsDestroyed(): Used to check if the native representation of the element has been destroyed.

LayoutRebuilder:

  • Responsible for rebuilding the layout of dirty UI elements. It is an implementation of the ICanvasElement interface
  • When a LayoutElement or LayoutGroup is marked as dirty, it's added to the LayoutRebuilder's list of objects to rebuild.
  • On the next frame, before rendering, LayoutRebuilder goes through its list and recalculates the layout for each dirty object. This ensures that the UI is always correctly laid out before it's drawn on the screen.
  • It works in conjunction with CanvasUpdateRegistry to determine the order of updates.

CanvasUpdateRegistry:

  • Manages the order in which UI elements are updated and rendered.
  • When a UI element needs to be redrawn (e.g., because its text or image changed), it’s registered with the CanvasUpdateRegistry.
  • The registry then ensures that all updates are processed in the correct order, so that parent elements are updated before their children, and so on.
  • LayoutRebuilder uses CanvasUpdateRegistry to ensure that layout calculations happen before any rendering.

Relationship:

  • LayoutElement and LayoutGroup both inherit from UIBehaviour, giving them a shared set of lifecycle methods.
  • When a LayoutElement or LayoutGroup changes in a way that affects layout, it's marked as dirty and registered with the LayoutRebuilder.
  • LayoutRebuilder uses the CanvasUpdateRegistry to ensure that all layout calculations are done in the correct order and before any rendering happens.

After delving into the code of the LayoutElement and LayoutRebuilder , the procedure for processing layout elements can be broken down as follows:

  1. Setting a LayoutElement as Dirty: When properties of a LayoutElement change in a way that might affect the layout (e.g., changing its preferred size, flexible size, etc.), it is marked as "dirty." This means that the layout needs to be recalculated to accommodate this change.
  2. Invoking LayoutRebuilder: Once a LayoutElement is marked as dirty, the LayoutRebuilder.MarkLayoutForRebuild method is called. This method's purpose is to determine the root of the layout that needs to be rebuilt. This is crucial because in a nested UI hierarchy, changing a child element might affect parent elements, and the entire layout hierarchy might need to be recalculated.
  3. Finding the Layout Root: The MarkLayoutForRebuild method traverses up the UI hierarchy to find the highest-level parent (or the root) that has a layout component (like VerticalLayoutGroup, HorizontalLayoutGroup, etc.) and is active. This root is the starting point for the layout rebuild process.
  4. Checking for a Valid Controller: Before proceeding, the method checks if the found layout root is a valid controller using the ValidController method. This ensures that the layout root has an active and enabled component implementing the ILayoutController interface.
  5. Registering for Layout Rebuild: Once a valid layout root is identified, it is registered for a layout rebuild using the CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild method. This method queues the layout root for a layout pass in the next frame.
  6. Actual Rebuild: In the subsequent frame, Unity processes all registered elements in the CanvasUpdateRegistry and recalculates the layout based on the changes.
Method-calling procedure:
An Example of Layout Procedure Fired from OnTransformParentChanged

The first performance issue emerges from the frequent invocation of this process, especially in dynamic UIs where elements are regularly updated or changed. Every time a LayoutElement is marked dirty, the system might have to (1) traverse the UI hierarchy, (2) identify the layout root, and (3) register it for a rebuild. This situation is further complicated, especially when you apply animations to the element that might influence the LayoutElement, intensifying the potential for performance degradation. If this rebuild process is triggered multiple times within a single frame, it can lead to significant performance overheads, particularly in intricate UI hierarchies.

The second performance issue is identified in LayoutGroup.CalculateLayoutInputHorizontal. The method invokes rect.GetComponents(typeof(ILayoutIgnorer), toIgnoreList); within a loop, which can become resource-intensive, especially when dealing with a significant number of child elements.

Inevitably, Unity’s built-in layout system, crafted for wide-ranging use cases, can act as a performance double-edged sword. Therefore to improve UI runtime efficiency, we can consider the following approaches:

  • Selective Child Processing: While Unity’s layout system is designed to be robust and user-friendly, it has to processes every child’s RectTransform, particularly in complex UI structures. A potential solution is manual management, enabling developers to specifically determine which children require layout adjustments.
  • Strategic Layout Management: Adopting a targeted re-layout strategy and batching layout changes can provide more precise control and efficiency. Not every UI modification necessitates an immediate layout update; optimizing the timing of these updates can significantly reduce performance overheads.

Therefore, a streamlined layout system is needed that, while perhaps not as universally applicable or user-friendly, meets specific demands with improved performance. In the subsequent article, we’ll endeavor to construct a lightweight version of the layout system, building upon Unity’s LayoutElement.

(to be continued…)

--

--