There is only one way to show a UI on a client, which is for the server to send the client some JSON describing the elements to create/destroy/update. However, there are multiple ways to create and send such JSON.
The simplest and most common way to create and send UI JSON is by modeling the elements and components in plain C# classes, creating a collection of temporary objects of those classes to describe the desired UI, then using the Newtonsoft JSON library to serialize that collection into a JSON string, and finally sending that string to one or more clients using a specific RPC call to the client. This approach is most commonly implemented using the Cui* classes provided by the Oxide framework. However, this approach leaves much room for optimization, which I will outline below.
- Creating short lived objects generates a significant amount of garbage (i.e., objects that are no longer referenced by other objects), which increases work for the garbage collector and increases the frequency of garbage collection lag spikes. This can be avoided by either pooling objects or by carefully using structs instead of objects.
- Creating JSON strings generates a significant amount of garbage. As strings are immutable in .NET, it's not possible to modify a string for reuse, meaning any time you need to send a different variation of the UI, you need to create an entirely new string. Since the RPC call to the client will eventually convert the string to bytes before sending, the string is not strictly necessary. You can cut out the intermediate string by serializing directly to bytes.
- Newtonsoft JSON serialization is relatively slow. Alternative libraries exist such as System.Text.Json, but that one is not available on Rust servers by default. You can also implement a custom JSON serializer fairly easily, on the basis that it only needs to support serialization (no deserialization), and only needs to support a few levels of object nesting (due to the limited CUI specification).
- Oxide's CuiHelper only supports sending a UI to one player at a time, resulting in duplicate string->bytes conversions if sending to multiple players. The duplicate string->bytes conversions can be avoided by making the RPC call directly, and passing it a list of player connections to send the data to.
To learn more about the CUI specification (i.e., what the client accepts), see the following documentation written by a community member (Kulltero).
https://github.com/Kulltero/Rust.Community/tree/Docs
Besides Oxide's CUI, various community members have created alternative server-side CUI implementations. Some private, some public. I'll list a few here for reference.
- https://github.com/dassjosh/Rust.UIFramework -- UI Framework created by a community member (MJSU).
- Custom serializer, 10x faster serialization than Newtonsoft
- Supports sending UI to multiple clients at once without duplicate string->bytes conversions
- Provides object pools for modeling UIs as objects without generating garbage
- Provides a vector cache to avoid generating garbage for repeat Vector3->string conversions
- Provides some higher level elements that abstract away the smaller CUI building blocks
- https://github.com/WheteThunger/Backpacks/blob/34f1be40daf295de79dc8f6a57798edd4fae5bbf/Backpacks.cs#L2248 -- Custom UI builder in the Backpacks plugin.
- Same improvements as the above framework, but does not provide higher level of abstractions
- Uses structs instead of pooled objects as the means to avoid garbage allocation, but this comes with some minor limitations
- Provides much closer syntax to the Oxide CUI library, making it relatively easy to transition older code
- k1lly0u has some sort of framework or library in his Chaos Extension used by many of his plugins, but I haven't looked at it, so unsure what it can do or if it's available for general use (it might be obfuscated and/or not licensed for 3rd party use).