Purpose: These are generalized optimization concepts designed to help you improve the performance of your VRChat worlds. By implementing these practices, you can free up system resources and allocate that performance budget to the visually engaging elements that enhance the user experience, or just overall providing a more performant user experience.
In Unity, almost everything you see or interact with in a scene is an object. Every one of these items is an instance of a GameObject.
A GameObject is essentially a fundamental building block. It holds a Transform component (which defines its position, rotation, and scale in the world) and is then given its specific behavior and functionality by attaching other Components to it.
This includes:
3D Models: Cubes, spheres, terrains, custom imported meshes, etc.
Lights: Directional lights, point lights, spot lights.
Cameras: The perspective through which the player sees the world.
Empty Containers: Used to organize other objects.
UI Elements: Buttons, text, images, and canvases.
When prototyping or outlining worlds, it's easy to build structures out of many separate GameObjects. It is crucial to be aware that every single GameObject consumes a portion of system resources, regardless of its state.
This cost applies even if the GameObject is:
Inactive (disabled).
Invisible (e.g., hidden behind walls or outside the view frustum).
Static or Animated.
While the cost of a single GameObject is usually minuscule, having hundreds or thousands can quickly contribute to overhead, impacting the overall CPU performance and memory usage of your world.
You will almost always gain a performance boost by merging multiple small, static GameObjects into one larger mesh. This optimization is primarily due to reducing the number of Draw Calls and simplifying lighting calculations.
When you have many separate GameObjects, the rendering engine must make a separate Draw Call for each object to tell the GPU what to render. Every draw call introduces overhead, straining the CPU.
By combining these objects into a single large mesh, the engine only needs one draw call to render the entire structure, significantly reducing the CPU load.
The performance boost from lighting comes from Batching. When objects are separate, the engine must perform intensive, per-object calculations for features like:
Shadow Mapping: Determining if and how each object casts or receives shadows.
Light Probes/Reflection Probes: Calculating how light interacts with the material of each individual object.
When objects are merged into one large mesh, these complex calculations are processed more consistently and efficiently across a single surface. This enables the engine to use efficient GPU batching methods, leading to smoother and faster rendering.
A common pitfall in Unity development is the performance impact caused by unnecessary data density in your assets. When downloading prefabs or Unity Asset Store models, it's easy to acquire assets with excessively high vertex counts and textures with resolutions well into the 4K range.
It is essential to manage the data density (vertex count and texture resolution) of your assets to be proportional to their significance in your project.
1. Model Resolution (Vertex Count)
Models are built from vertices (points) and planes (triangles). A model with thousands of vertices is known as a high-poly model and demands more resources from the CPU and GPU to process and render.
An object that will only be viewed from a distance does not need a high-poly count.
By reducing the mesh complexity (using a lower-poly version or LODs), you can save a significant portion of your rendering budget.
2. Texture Resolution
Texture resolution (e.g., 4K, 2K, 512x512) directly affects the amount of VRAM (Video RAM) required on the user's graphics card.
High-resolution (4K) textures should be reserved only for objects that are large, central, or viewed up close by the player.
For less important items, or those viewed far away (like background props), you can save a lot of performance budget by compressing or downscaling the texture resolution to a more reasonable level (e.g., 1K or 512x512).
By correctly balancing the detail of an asset with its importance in the scene, you ensure that you are spending your valuable performance budget where it will have the greatest visual impact.
Static batching combines the mesh data of multiple static objects (objects that won't move, rotate, or scale during gameplay) into a single, larger mesh at editor or build time.
Static Objects: For an object to be eligible for static batching, you typically need to mark it as "Static" in the game engine's settings (e.g., in Unity). Examples include buildings, terrain features, rocks, and environmental props.
This minimizes the number of times the CPU has to tell the GPU, "Draw this object, then draw that object, then draw the next object..." By combining them, the CPU can issue one draw call for the entire batch of objects.
Draw Calls: Each draw call has an overhead. Reducing them significantly reduces CPU workload and can lead to major performance improvements, especially on scenes with a high number of small static elements.
For static batching to work, the static objects being batched must:
Be marked as Static.
Share the exact same Material. This means they must use the same shader and textures.
Not have been moved, rotated, or scaled from their original position since the batch was created.
Managing how often your code executes is critical for performance. While Unity's built-in functions like Update() are convenient, relying on them too heavily can quickly and unnecessarily bog down the user's hardware.
Functions like Update() and FixedUpdate() cause your code to run every frame or every physics step, respectively. If you use these functions to check object states (a practice called polling), the hardware is constantly burdened with checks that often return the same result.
For important, central processes (like player movement or complex physics), using Update() may be necessary and acceptable. However, using it for routine state checks, such as:
Is the door open?
Is the player standing here?
Has the object's color changed?
...can severely degrade performance.
The key to a more performant user experience is to avoid polling and transition to event-driven programming.
While it can be more complex to catch every change only when it happens, this approach is vital for performance. Instead of checking a state every frame, the code only executes when a change occurs (an "event").
Example: Instead of checking in Update() if a button is pressed, the button script itself triggers a specific function only when the collision or interaction occurs. In VRChat a good example is the Interact() function available for colliders.
This design pattern ensures that functions are called only when they are needed, significantly reducing the computational load on the end-user's machine.