QuizU: UI Toolkit performance tips

Hi everyone,

Welcome to our last post in our series of articles that explain in depth the techniques used to create the QuizU sample project.

Thanks to all of you that have followed along and even posted your questions and comments!

As we’ve said before, QuizU is meant to be an instructional sample that teaches techniques for implementing programming design patterns into your game UI.

List view

The other articles in this series are here:

  1. Welcome to the new sample project QuizU
  2. QuizU: State pattern for game flow
  3. Managing menu screens in UI Toolkit
  4. Model View Presenter pattern
  5. Event handling in UI Toolkit

Make sure you download the project from the Unity Asset Store and that you’re running on 2022 LTS to follow along.

Also, ICYMI, QuizU is not our only UI Toolkit demo. It complements two big pieces of content we released last year to help you get started with UI Toolkit:

  • UI Toolkit sample – Dragon Crashers: This demo is on the Asset Store. It’s a slice of a full-featured interface added to the 2D project Dragon Crashers, a mini RPG, using the Unity 2021 LTS UI Toolkit workflow at runtime.
  • User interface design and implementation in Unity: This free e-book covers UI design and art creation fundamentals, and then moves on to instructional sections on UI development in Unity, mainly with UI Toolkit, but also with a chapter covering Unity UI.

Today’s post provides you with some general optimization tips when using UI Toolkit.

Building complex UIs with UI Toolkit can enhance performance, but you’ll still want to be mindful of best practices when deploying it.

Consider these tips and techniques for getting the most out of UI Toolkit.


Pre-create hierarchies rather than instantiating at runtime.

Hierarchy and elements

Pre-create hierarchies: Instantiating templates at runtime can slow your application. Pre-create your hierarchies in the Editor or during loading or non-peak periods to optimize performance.

Keep the visible element count low: More visible elements require more processing. Try to keep your UI lean and only display what is necessary.

Use DisplayStyle.None for hidden/offscreen elements: Instantiating a complex hierarchy of elements is a relatively slow operation. Pre-creating UI elements and setting them to DisplayStyle.None can improve performance, but balance this with memory usage.

Pool recurring elements: For elements that frequently appear and disappear, utilizing a pooling system can be beneficial. Rather than constantly creating and destroying these elements, you maintain a set of them in a pool. When needed, you simply reuse and recycle.

Use ListView for lists: ListView provides built-in pooling functionality, which can significantly improve performance for list-type UIs. ListView only renders the visible elements and recycles them as you scroll through the list, making it an efficient choice for long lists.


ListView provides built-in pooling for performance.

Implement custom culling: For non-ListView elements, you can implement custom culling using GeometryChangedEvent and VisualElement.layout property to manage the visibility and size of child elements.

Listen for the GeometryChangedEvent to know when a visual element’s layout (size/position) property changes. If so, then check the VisualElement.layout against the container displaying it. If the element is outside of this visible area, you can cull the child element by making it invisible.

Leverage UsageHints: Use the usageHints property to inform the system about how an element will be used at runtime. UsageHints can help optimize data storage and performance.

For instance, if you know that a particular element is going to be frequently updated (e.g., a score counter), you might set its UsageHints to DynamicTransform or DynamicMaterial, which tells the system to optimize the element for frequent updates.

Set the usageHints property before the VisualElement is part of a Panel. Otherwise, it is read-only.

Asset loading

Optimize asset loading: Load assets or scenes during opportune moments, such as loading screens or transitions, to minimize disruptions to gameplay.

Understand UXML/USS dependencies: UXML/USS assets preload their dependencies, such as images referenced from the UI Builder. This is generally a good thing, but it can lead to inefficiencies if not managed correctly. Be aware of these dependencies to avoid unnecessary redundancy.

Mind your textures: Excessive textures can lead to an increase in draw calls. Be smart about texture usage and batching to maintain smooth rendering performance.

Utilize dynamic atlas systems: Small UI textures like icons can be added to an in-memory texture atlas. Misconfigured textures, however, can lead to unnecessary batch breaks. Doublecheck texture import settings, texture formats, mipmaps, non-power of two sizes, and compression settings when setting up a dynamic atlas.

Group textures with Sprite Atlases: You can use Sprite Atlases to group together textures used at the same time, which might be preferable to an auto-managed atlas.


Check texture memory in the Profiler.

UQuery

Cache Query results: UQuery requires traversing the hierarchy, which can be computationally expensive. Cache your query results for reuse to reduce overhead.

Leverage UQueryBuilder and the UQueryState struct: Avoid creating lists and optimize your queries by using UQueryBuilder and UQueryState. UQueryBuilder is a class used to construct queries to select specific UI elements based on various constraints like name, type, or class. UQueryState represents the result of an executed query. It contains the matching UI elements and allows iteration and manipulation of these elements.

For large UIs, querying for elements using UQuery can be more efficient than iterating over a list, especially when the query results in a small subset of elements. Unlike a list, the UQuery system does not require a full iteration.

Garbage collection

Be mindful of garbage collection: Unity’s garbage collection can feel like a black box, but try not to retain references to visual elements in classes that outlive the UIDocument or Window where they originated.

Be cautious about closures and lambda functions, which might accidentally retain a reference to a visual element; that visual element won’t be garbage collected as long as the closure exists. This can accidentally lead to memory leaks and performance issues if not managed carefully.

Incremental GC can be beneficial: With high memory pressure, GC spikes can occur. In the Project Settings window, find the Configuration section, and check the box next to Enable Incremental GC to help alleviate this.


Enabling Incremental Garbage Collection can help reduce memory pressure.

Rendering

Understand UI Toolkit’s shader approach: UI Toolkit uses a single shader approach, so each draw call isn’t as costly as a full material change. Understand the trade-offs to optimize your UI (e.g. you might not be able to incorporate certain visual or shader effects into your UI if they require a different shader).

Avoid expensive tessellation: Any time the size (width/height) of an element changes, its geometry re-builds. This can be computationally expensive.

Instead of frequently changing the size of UI elements, you can use transforms for animations or adjustments. Transforms are less performance-intensive as they don’t trigger tessellation.

Similarly, using textures or 9-sliced sprites can be a better choice for rounded corners or borders. These alternatives do not trigger tessellation and can be more efficiently reused across different UI elements.

Choose the right font population mode: Use Static Font Assets for known text, such as labels and titles. They are fast and efficient, as they pre-bake characters into the atlas texture and don’t require the source font file at build time. Dynamic OS Font Assets reduce memory overhead since the font source files don’t need to be included in the build. Dynamic Font Assets incur additional performance overhead because the source font files must be included in the build.

Choose the right atlas mode for fonts: In terms of rendering, use Bitmap for pixel-perfect alignment, ideal for pixel art. For fonts that might undergo transformations or need special effects like outlines, opt for Signed Distance Field (SDF) rendering for a crisper and more adaptable presentation.

Styles and selectors

Simplify USS selectors: Complex hierarchical selectors can negatively impact performance. Use the Block-Element-Modifier convention to reduce complexity, and avoid the “*” selector as it requires every potential element to be tested against the selector.

USS Selector
USS selector cheat sheet (avoid the * selector).

Use :hover sparingly: While :hover can enhance interactivity, it can trigger a redrawing of the hierarchy of targeted elements and their children. This could lead to significant performance overhead, especially if the hierarchy is complex or the changes are frequent.

Avoid inline styles: Inline styles have per-element memory overhead and require more processing to resolve the final style of an element. Favor using USS rules for efficient memory usage and better performance.

Balancing Performance and Usability

Of course, remember that optimization is both an art and a science. Some of these suggestions may lead to increased code complexity or a change of workflow, so factor that in before attempting a specific technique.

For more general tips about performance optimization in UI Toolkit, be sure to see the manual page in the documentation.


Be aware of complex hierarchies when using the :hover pseudo-state

Remember to profile your UI to identify potential performance bottlenecks and optimize based on your specific project needs. Optimization often involves a trade-off between performance and usability. Always strive to strike the right balance for your specific use case.

In most cases, you will need to profile to see how these techniques can tangibly improve your specific project. Then, decide as a team which ones to implement.

Thanks for reading! Let us know what you think of this article, or the entire series, as well as the QuizU sample project.

We hope you find these resources helpful!

3 Likes

After registering ClickEvent for a button, the delegate needs to access variables outside the lambda scope, such as a scoreNum, in which case closures are unavoidable. What should I do with it so that the button doesn’t release?

Also, there is no advanced use case for ListView, neither of the two official examples I looked up used ListView. I want to know bind best practices for complex objects (not a label)

2 Likes

First of all, thanks for the nice series. It was a very informative and interesting read.

I personally would have liked to have an example for the ListView in any of the two projects as well. I mean it would have been great to see applications of the ListView in the context of the level selection in QuizU or maybe the shop or inventory in Dragon Crashers to have a reference of what best practices are for them.

2 Likes

Thank you. Great feedback. We will definitely consider including ListView for the next update.

1 Like