
Removing empty callback definitions
The primary means of scripting in Unity is to write callback functions in classes derived from MonoBehaviour, which we know Unity will call when necessary. Perhaps the four most commonly used callbacks are Awake(), Start(), Update(), and FixedUpdate().
Awake() is called the moment MonoBehaviour is first created, whether this occurs during scene initialization or when a new GameObject instance containing the MonoBehaviour component is instantiated at runtime from a Prefab. Start() will be called shortly after Awake() but before its first Update(). During scene initialization, every MonoBehaviour component's Awake() callback will be called before any of their Start() callbacks are.
After this, Update() will be called repeatedly, each time the rendering pipeline presents a new image. Update() will continue to be called provided MonoBehaviour is still present in the scene, it is still enabled, and its parent GameObject is active.
Finally, FixedUpdate() is called just before the physics engine updates. Fixed updates are used whenever we want activity similar in behavior to Update() but that isn't tied directly to the render frame rate and is called more consistently over time.
Whenever a MonoBehaviour component is first instantiated in our scene, Unity will add any defined callbacks to a list of function pointers, which it will call at key moments. However, it is important to realize that Unity will hook into these callbacks even if the function body is empty. The core Unity Engine has no awareness that these function bodies may be empty and only knows that the method has been defined and, therefore, that it must acquire it and then call it when necessary. Consequently, if we leave empty definitions of these callbacks scattered throughout the code base, then they will waste a small amount of CPU due to the overhead cost of the engine invoking them.
This can be a problem since, anytime we create a new MonoBehaviour script file in Unity, it will automatically generate two boilerplate callback stubs for us for Start() and Update():
// Use this for initialization
void Start () {
}
// Update is called once per-frame
void Update () {
}
It can be easy to accidentally leave these empty definitions on scripts that don't actually need them. An empty Start() definition is liable to cause any object to initialize a little more slowly, for no good reason. This effect may not be particularly noticeable for a handful of MonoBehaviours, but as development on the project continues and we populate our scenes with thousands of custom MonoBehaviours with lots of empty Start() definitions, it could start to become a problem, causing slow scene initialization and wasting CPU time whenever a new Prefab is created via GameObject.Instantiate().
Such calls typically happen during key gameplay events; for instance, when two objects collide, we might spawn a particle effect, create some floating damage text, play a sound effect, and so on. This can be a critical moment for performance because we've suddenly requested that the CPU makes a lot of complicated changes, but with only a finite amount of time to complete them before the current frame ends. If this process takes too long, then we would experience a frame drop as the Rendering Pipeline isn't allowed to present a new frame until all of the Update() callbacks—counted across all MonoBehaviours in the scene—have finished. Ergo, a bunch of empty Start() definitions being called at this time is a needless waste and could potentially cut into our tight time-budget at a critical moment.
Meanwhile, if our scene contains thousands of MonoBehaviours with these empty Update() definitions, then we would be wasting a lot of CPU cycles every frame, potentially causing havoc on our frame rate.
Let's prove all of this with a simple test. Our test scene should have GameObjects with two types of component, EmptyClassComponent, with no methods defined at all, and EmptyCallbackComponent, with an empty Update() callback defined:
public class EmptyClassComponent : MonoBehaviour {
}
public class EmptyCallbackComponent : MonoBehaviour {
void Update () {}
}
The following are the test results for 30,000 components of each type. If we enable all GameObjects with attached EmptyClassComponents during runtime, then nothing interesting happens under the CPU Usage area of the Profiler. There will be a small amount of background activity, but none of this activity will be caused by EmptyClassComponents. However, as soon as we enable all objects with EmptyCallbackComponent, we will observe a huge increase in CPU usage:

It's hard to imagine a scene with more than 30,000 objects in it, but keep in mind that MonoBehaviours contain the Update() callback, not GameObjects. A single GameObject instance can contain multiple MonoBehaviours at once, and each of their children can contain even more MonoBehaviours, and so on. A few thousand or even a hundred empty Update() callbacks will inflict a noticeable impact on frame rate budget, for zero potential gain. This is particularly common with Unity UI components, which tend to attach a lot of different components in a very deep hierarchy.
The fix for this is simple: delete the empty callback definitions. Unity will have nothing to hook into, and nothing will be called. Finding such empty definitions in an expansive code base may be difficult, but if we use some basic regular expressions (known as regex), we should be able to find what we're looking for relatively easily.
The following regex search should find any empty Update() definitions in our code:
void\s*Update\s*?\(\s*?\)\s*?\n*?\{\n*?\s*?\}
This regex checks for a standard method definition of the Update() callback, while including any surplus whitespace and newline characters that can be distributed throughout the method definition.
Naturally, all of the preceding is also true for the non-boilerplate Unity callbacks, such as OnGUI(), OnEnable(), OnDestroy(), and LateUpdate(). The only difference is that only Start() and Update() are defined automatically in a new script.
It might also seem unlikely that someone generated so many empty versions of these callbacks in our code base, but never say never. For example, if we use a common base class, MonoBehaviour, throughout all of our custom components, then a single empty callback definition in that base class will permeate the entire game, which can cost us dearly. Be particularly careful of the OnGUI() method, as it can be invoked multiple times within the same frame or UI event.
Perhaps the most common source of performance problems in Unity scripting is to misuse the Update() callback by doing one or more of the following things:
- Repeatedly recalculating a value that rarely or never changes
- Having too many components perform work for a result that could be shared
- Performing work far more often than is necessary
It's worth getting into the habit of remembering that literally every single line of code we write in an Update() callback, and functions called by those callbacks, will eat into our frame rate budget. To hit 60 fps, we have 16.667 milliseconds to complete all of the work in all of our Update() callbacks, every frame. This seems like plenty of time when we start prototyping, but somewhere in the middle of development, we will probably start noticing things getting slower and less responsive because we've gradually been eating away at that budget, due to an unchecked desire to cram more stuff into our project.
Let's cover some tips that directly address these problems.