By implementing these targeted optimizations for UGUI’s underlying mechanisms, the opening time of complex interfaces can be reduced from 600–800ms to approximately 100ms.
1. VertexHelper Data Thinning
By default, VertexHelper processes all vertex attributes. However, most UI elements only require Position, Color, and UV0.
- Optimization: Introduce a
UGUIVertexTypebitmask (Flags) to only clear and fill necessary vertex attributes. - Impact: Significantly reduces the number of
Addoperations on internal Lists and lowers memory overhead.
C#
[System.Flags]
public enum UGUIVertexType {
E_Position = 1 << 0,
E_Color = 1 << 1,
E_Uv0 = 1 << 2,
// ... other attributes
E_Default = E_Position | E_Color | E_Uv0,
E_All = 0xFF
}
2. Rebuild Queue Optimization (IndexedSet)
The m_LayoutRebuildQueue and m_GraphicRebuildQueue in CanvasUpdateRegistry are heavy due to frequent dictionary swap operations used to move active nodes to the head and inactive nodes to the tail.
- Optimization: Since the data is cleared after a single iteration, complex swapping is unnecessary. Simply check if a node is active during the iteration process.
- GraphicRegistry: If your project doesn’t strictly require
m_Graphicstracking, you can bypass this to further reduce overhead.
3. Hierarchy Lookup Efficiency
- Optimization: Replace
IsDescendantOrSelfwithIsChildOf. - Reasoning:
IsDescendantOrSelfinvolves deep recursive parent lookups.IsChildOf(or manual depth-limited checks) is often more performant for standard UI validation.
4. ListPool vs. Static Fields
- Optimization: For high-frequency operations, replace
ListPoolwith Static Fields. - Reasoning: While
ListPoolprevents GC, the management overhead of the pool itself can become a bottleneck under extreme frequency.
5. Canvas Caching in GraphicRegistry
Methods like DisableRaycastGraphicForCanvas and UnregisterRaycastGraphicForCanvas are time-consuming because they frequently call TryGetValue to find the parent Canvas.
- Optimization: Implement Canvas Caching. Since unregistered/disabled nodes are often siblings, their parent Canvas is likely the same as the previous node, allowing you to skip redundant dictionary lookups.
6. Generic vs. Type-based Component Access
- Optimization: Convert generic
GetComponent<T>orGetComponents<T>calls to theirType-based counterparts (e.g.,GetComponent(typeof(T))). - Reasoning: In certain engine versions and low-level frameworks, passing the
Typedirectly can bypass some generic resolution overhead.
7. Lightweight Dictionary Implementation
- Optimization: Implement a custom, lightweight Dictionary for critical paths.
- Reasoning: The native
System.Collections.Generic.Dictionaryis robust but heavy. A specialized implementation can reduce the overhead ofEqualityComparercalls and internal bucket management.
8. Batching Large-Scale Tiled Elements
In scenarios like “Comic Walls” where hundreds of Image nodes are used for tiling:
- The Problem: Hundreds of
Imagecomponents lead to massive instantiation andOnEnablecosts. - The Solution: Use
CanvasRenderer.SetMeshto assemble these tiles manually. - Overcoming Limits: Although
CanvasRenderersub-meshes are limited to 8, you can merge vertices sharing the same texture into a single mesh. This reduces hundreds of nodes down to a few, enables SRP Batcher compatibility, and significantly lowersSetPasscalls.
9. On-Demand Loading for “Red Dot” Systems
Art teams often attach “Red Dot” notification nodes directly to every UI element, leading to many hidden nodes (e.g., 60+ hidden red dots on a main menu).
- Optimization: Load red dots on-demand at runtime.
- Refinement: Use a component-based structure rather than full GameObject hierarchies to keep the UI tree lean. This drastically reduces the total node count and the cost of UI hierarchy traversal.