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