GUI Optimization tips?

I’m currently in the process of making GUI for a relatively large project. There are dozens of different screens and menus, so it is quite extensive. I currently have all the GUI in a separate place file, and I’m going to import it into the main game and hook it all up to the backend, but before I do that I’d like to make sure it’s optimized as much as possible, so I’m wondering if there are any general optimization tips for GUI.

One thing I’m especially concerned about is if having too many local scripts is a problem. I have many different buttons which all have local scripts under them for tweening animations. I know I can just use collection service instead of having a localscript under each button, but I’m wondering if there’s any difference in performance.

There isn’t a lot of optimization that goes into UI. Quite the opposite; if you managed to get it to work, then don’t bother optimizing it. But if you really wish to squeeze as much performance as possible:

  • Avoid canvas groups
  • Use same gui instance for 2 purposes, like background and a button
  • (Unrecommended) use a single ScreenGui
1 Like

Why not canvas groups?
And I was under the assumption that separating menus into different ScreenGUIs was a good thing for performance.

Canvas Groups cause a huge hit on memory. And separate screen GUIs render differently instead of batching them in a single render.

2 Likes

Well that’s a shame. Canvas groups are functionally incredibly useful because of GroupTransparency. Guess I’ll cut down on those. Putting everything into one ScreenGUI though is something I’d rather not do for organization purposes, but if it increases performance I guess I’ll do that.

Their purpose is mostly to act as clipping masks to objects that are not compatible with ClipDescendants, such as rotated frames, etc.
For rest, you should never ever use them.


You can use this technique to set properties of UI elements faster: Narrowing the LuaBridge overhead (old)
You can build UI elements from code (using the method I sent above combined for better results) to have more control.

I recommend you build a UI using Roblox’s UI editor and then compile it into code using a plugin such as this:

Make sure Fast Namecalls are set so it uses Narrowing LuaBridge for compiled code


image

1 Like

Yeah so I’ve thought about using a code based approach with something like Roact, but I personally prefer Roblox’s built in UI editor. Using the plugin you sent seems useful but is there any actual performance improvements in using it? Also, I have multiple local and module scripts embedded into my UI for animations and loading data onto the GUI, would using the plugin break any of that?

Putting everything in a single ScreenGui is like the worst thing you can do, since any update in any frame would rerender the entire ScreenGui.

2 Likes

Well now I’m getting conflicting responses here :sob:

Don’t use React.
It will do the opposite; it’s super bloated and actually makes it less readable.
Use clean Luau because it’s the most optimized and super readable already.

local new = Instance.new
local Frame = new "Frame" do
Frame.Name = "hi"
end

The plugin I sent compiles instance hierarchy into code ahead of time; no need for any bloat, the code it produces runs without any integrations.

Really, GUIs aren’t too expensive. Just keep an eye on DrawCalls and focus on following MrRobotChicken’s advice in his optimizations thread here: Real world building and scripting optimization for Roblox - Resources / Roblox Staff - Developer Forum | Roblox

CanvasGroups aren’t bad really in careful use either really. I did find some issues you’ll run into though, so just be mindful that:

  • Low Graphics Settings (1-4) result in blurry GUIs in CanvasGroups (rendering at 480p or lower sometimes)
  • A device running out of memory will fail to render CanvasGroups and instead substiute the contents with a white texture.
  • Most of the time, you can achieve the same thing with regular frames and careful use of UI modifiers and UI layout tools mixed with some careful Luau as you can with Canvases.

Also, most of the GUI Optimization isn’t as much in the design as it is in the programming. It’s also about prioritizing User Experience, Seamelssness and Load Times. You can make use of already included property data from objects within your GUI Hierarchy:

  • UIGridLayout and UIListLayout have AbsoluteContentSize. This is most reliably read after the GUI’s content has changed, or after the GUI is made visible. Otherwise, you can run into times where the property reads (0, 0) because it didn’t get chance to update based on visible content.

  • AbsoluteSize and AbsolutePosition exist too. These are super useful if you’re working with specific pixel offsets or targeting specific devices. Don’t be shy about using them, just know they aren’t as useful as Scale values are.

  • UIScale and UIAspectRatioConstraint are super useful too. If you need to constrain or rescale something, you’re better off with these.

  • If you are looking for the lowest common denominator for what constitutes as a renderable GUI component, use myGuiObject:IsA("GuiObject") in conditionals.

  • If you are trying to make something like a tech tree, or a list of purchasable store items, don’t keep references to them in memory for longer than you absolutely need to. Delete objects and destroy connections for things that assemble from a template.

    • If a GUI is temporary, like a match end screen, you can often just clone and populate a template ending screen, then delete it and disconnect any events once the ending screen container is hidden. Just make sure to nil any temporary runtime references to the screen and any RBXScriptConnections afterwards.
    • If you are animating things with TweenService, You can also manually destroy and nil references to tweens. They are objects too.
  • Multiple scripts per-se aren’t even bad. It’s better to have multiple scripts that control context-specific GUI elements than it is to have a monolithic design.

    • Example: Instead of having a GameplayGuiManager, you can split it up into corresponding components like GameplayMinimapManager, GameplayVitalsManager, GameplayObjectivesManager. It’s common in Unity Engine and Unreal Engine, but becomes extremely useful when quick-debugging. You trade off some extra memory usage, and extra object references in memory for ease-of-navigation and separation of concerns.
  • ROBLOX Primitives (Frames) are faster to render than ImageLabels, even with the use of ContentProvider to preload assets at the loading stage. The content still has to be loaded into memory which takes more time in ROBLOX’s engine. If you can achieve the same results with one or two more frames, and objects like UICorner and UIGradient, it’s better to do that instead because there’s no assets being loaded or unloaded.

    • You can still preload assets with some success to somewhat mitigate loading times for assets, but it’s hit and miss. Sometimes they’ll load faster, sometimes they won’t.
  • A special note needs to be made that ImageLabels with Slice (and possibly Tile) don’t work with UICorner. They result in unique artefacts because of how the engine processes the slices.

  • There’s not much difference between having a pre-assembled GUI and one assembled by a script. This isn’t to say there aren’t advantages to libraries like Roact, ReactLua or Flux, some do fix performance issues with things like ScrollingFrames, but it’s a trade off of maintainability, visual editing and performance.

    • If you prefer visual editing, stick with the Visual Tools. If you prefer literal editing (i.e. script-based), then stick with Luau scripting and/or any libraries you use. It really doesn’t matter to the end user, just to you. So don’t get hung up on the details.
  • If you need to share module scripts with GUIs then for the love of all that is Luau put the module scripts somewhere in ReplicatedStorage, especially if the GUI resets on spawn! ROBLOX treats clones of ModuleScripts as unique instances and caches each require of them separately. Just because it’s the same script doesn’t mean it’s the same instance, and the cached result will stay in memory until the player leaves.

That’s what I’ve run into from personal experience from 5 years of GUI and Tools Programming over 3 engines.

3 Likes

Sometimes it’s more readable, sometimes it’s not. On Roblox, I can’t even imagine using it. On the web, I still can’t imagine using it. Not because it’s inherently unreadable—I like JSX—I just can’t, for the life of me, like React. It just feels weird. The closest I’ve gotten to writing React is writing Dioxus in Rust. I’ve never actually finished a project in React.

1 Like

Adding onto this, I have finished projects in Angular. Just, not React. I don’t know what it is about React, but I can’t bring myself to write it.

Please never do this. This isn’t clean, readable Luau.

Caching globals in the local environment (script/function/block) doesn’t really work with Luau, as it’s an optimization the Luau team have repeatedly said (from memory) doesn’t apply to Luau like it does in Lua. So many people do this without ever reading Luau’s page on Optimization, namely because of this Lua Performance Tips document.

Luau on the globals caching trick:


Lua Performance Tips by Roberto Lerusalimschy, this excerpt oft being erroneously applied to ROBLOX:

In short, if you’re optimizing for Luau then always read the Luau website first. The Luau website is the go-to source for optimizing for Luau first, then Lua when Luau doesn’t cover a case.

And… just because you can call functions without brackets when the first parameter is a string too, don’t. It makes function calls and differentiating scopes less coherent at a glance.


Edit: I noticed the screenshot cutoff improperly due to my browser, so I added the full context properly*

2 Likes

Thank you, this was very informative :folded_hands:

1 Like

You don’t know what you are talking about.
Me King Yarik the Great optimizer of hot loop know luau better than any of yall in this thread :sunglasses:


Without satire thing i did literally changes nothing
It just makes visual look cooler
Its pure syntax sugar that do not change the bytecode at all

Also i fully disagree
Luau website tells you half truth
Only thing i trust is luau gitbub repo and bytecode.
Namecalls are ALWAYS evil, stop lying.

I don’t care what fancy text says, i care what fancy luau source code says :joy:

While not literally free you can assume multiple scripts to have no overhead for most practical purposes. It’s similar to just creating a coroutine or extra task, the major difference is that memory can’t as easily be reused as each script runs in a different context but that usually isn’t a problem and modules can fix that anyways. The contexts also take memory but really if you have memory issues in the game, it’s highly unlikely using multiple scripts is the issue.

Not a problem at all and can be a good thing for organization so long as the scripts remain simple and are only event connections when not actively running. I personally prefer the single script approach that loads data describing these things. I like it because it lets me reuse code better and just describe the differences as data which reduces reused code and tends to scale better. But this is an ease of development thing, not strictly an optimization thing for the most part.

I wouldn’t overthink optimizations. Build it the cleanest way you can and come back if it isn’t performing correctly AND you’ve experimentally confirmed UI is the problem. But ultimately clean code is better than code that would already run fast enough running slightly faster.

1 Like

Where did you get this from lol?

I think there was a reply from a mod somewhere as well, but I cant find it

1 Like