Short Summary on script optimizations

After applying these optimization it can take a huge toll on ability to read the script

This guide mostly covers optimizations that you can apply in your post-production code. These won’t change any existing algorithms.

I decided to separate this guide into levels; each level makes your code more and more unreadable.


Level 0

At this level I simply wanted to tell you that the thing that actually will decide how fast your game is is the algorithms you use for it. The methods below are considered micro-optimizations for the most part and will not improve the performance significantly unless it’s some edge case where these optimizations will help with your bottleneck.

Also, unlike many guides, this one won’t advertise native code generation. It’s not that good.

Worth mentioning that if you are able to read Lua source code, please read it. It can provide a lot of details if certain optimizations are worth doing. But for those who don’t, here I will include Lua optimizations that are simply not worth doing in Luau, but be aware that all of these optimizations will only work on ⁣--!optimize 2:

BAD:

local table_insert = table.insert
local math_pi = math.pi
local Instance_new = Instance.new

Luau already does global caching. If the global is highlighted blue, that means it is able to be automatically folded:

--!optimize 2
table.insert()
math.pi
Instance.new() -- not highlighted in forum but is highlighted in roblox's editor

BAD:

local object = {} -- example can be any standard object orientated metatable or just a table
object.__index = object
function object:SomeFunc(...)
   print(...)
end
local actualObject = setmetatable({},object)

local object_SomeFunc = actualObject.SomeFunc
object_someFunc(actualObject,"Hello world")

There is no need to cache custom-made object functions if you call them trough :. Luau does some voodoo optimizations that basically make so-called “namecalls” be extremely fast. So just call them normally:

--!optimize 2
local object = {} -- example can be any standard object orientated metatable or just a table
object.__index = object
function object:SomeFunc(...)
   print(...)
end
local actualObject = setmetatable({},object)

actualObject:SomeFunc("Hello world")

Beware that in some cases this optimization might be required still if the __index metamethod in question is not a reference to the parenting table but a function

There are a lot more of such Level 0 optimizations so just refer to this


Level 1

Level one includes optimizations that do not exactly cause unreadability in code and, in fact, quite the opposite, will make it a bit more readable.

Case 1:

Replacing deprecated globals with updated, newer ones will not only most likely optimize your existing code but also will keep it up to current standards. These include, but are not limited to:

wait() -- or Wait()
delay() 
spawn()

Please replace them if you haven’t already.

Case 2:

while task.wait() do
   script.Parent.Parent.SomeObject.Name = ""
end

If you frequently reference the same instance like this^, it will cause some performance loss; instead, cache it:

local SomeObject = script.Parent.Parent.SomeObject
while task.wait() do
   SomeObject.Name = ""
end

Level 2

Level 2 includes optimizations that can only be applied in certain cases

Case 1:

-- assume it is a server script
local Part = workspace:WaitForChild("Part")
-- assume it is a client script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Parent = ReplicatedStorage:WaitForChild("Part")

In the first example, there is absolutely no need for any WaitForChild in server scripts; server scripts will always run after everything has been loaded. However, in the second, it only applies to ReplicatedStorage and ReplicatedFirst if your client script is located in StarterPlayerScripts. In other cases, continue to use WaitForChild as you would usually:

-- assume it is a server script
local Part = workspace.Part
-- assume it is a client script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Parent = ReplicatedStorage.Part

Case 2:
This section doesn’t exactly include any exact examples; it is more of a rule that if you know something will be constant throughout the execution, then you better cache it. These include, but are not limited to:
BAD:

local t = table.create(1000) -- this is also an extremely useful function, use it to pre allocate
for i = 1,1000 do
   t[i] = script.Name
end

This will invoke the dot operator 1000 times; if we know it will be constant, then cache it:

local t = table.create(1000) -- this is also an extremely useful function, use it to pre allocate
local Name = script.Name
for i = 1,1000 do
   t[i] = Name
end

BAD:

while task.wait() do
   local info = RaycastParams.new()
   workspace:Raycast(Vector3.zero,Vector3.one,info)
end

DO:

local info = RaycastParams.new()
while task.wait() do
   workspace:Raycast(Vector3.zero,Vector3.one,info)
end

Level 3

This section includes aggressive optimizations; they are not recommended unless it is the bottleneck.

Case 1:

workspace.Name

Surprised? Well, actually speaking, the . operator is actually slow! Instead of just normally indexing like with a table or metatable, indexing any Roblox data type requires 2 steps: the first is receiving the metamethod of the instance, and the second is actually invoking it. On a massive scale, this is just an unnecessary take on performance. To circumvent this, we either can use GetPropertyChangedSignal or RawLib

local Name = workspace.Name
workspace:GetPropertyChangedSignal("Name"):Connect(function()
   Name = workspace.Name
end)

OR

local RawLib = require("./RawLib")
local instance_index = RawLib.instance_index

instance_index(workspace,"Name")

This actually spreads not only to instances but also to other data types like CFrame, Color3, and Vector2. As of writing, though, Vector3 is actually not a table but a native data type called vector, which removes such overhead. RawLib includes raw metamethods for other data types besides the instance; it works especially great for CFrames.

Case 2:
This is an extension for the first case. Because Roblox data types __index metamethod is actually a function, namecall optimizations will not apply:

workspace:Raycast()

To circumvent this, you as an exception actually have to reference the methods:

local Raycast = workspace.Raycast
Raycast(workspace)

This also applies to other roblox data types like RBXScriptSignal, UDim etc.


Summary

This guide actually turned out to be a lot smaller than I thought. In any way, these are not the best optimization practices, as they were meant to be applied in your post-production code. Use them carefully

EDIT 1: Fixed some grammar and added a note regarding Level 0 optimizations

20 Likes

Thanks!! I’ll bookmark this for later (I’m working on getting better at script optimization). It’s really nice of you to make this for free!

Is there any performance difference between using a defined function and an anonymous function?

As far as I know, no. Both of them are objects that you simply store a reference to. Although you still need to consider dynamic functions, which are possible even without loadstring. These can cause memory issues

1 Like