Let's talk lag -- What causes it? How do we stop it?

Hey folks! Looks like you stumbled across an old post of mine. Be weary that some information may be outdated!

Original Post

Hey there!

Even after being on Roblox for a decade now, I struggle with fighting performance problems with poor memory management chewing away at framerate and internet response time.

It seems that some of my games, especially the ones with my weapon system, has a TON of issues keeping that 60 FPS in the stats.

Which of these cause lag? Which of these should I worry most about?

  • coroutine (should I use spawn. Or should I use wrap instead of resume(create)?)
  • Weak tables (for internal modules and other data)
  • Module scripts
  • Storing Roblox metamethods, functions, and instances as variables in the script (see Figure 1)
  • Use of pcall
  • Usage of LuaU syntax (e.g. local bruh:number = 5)
  • Usage of delay
  • Large scripts (800-2000 average-sized lines of code)
  • Instance connections such as Died, Changed, and Heartbeat.
  • Usage of JSON encoded strings rather than tables.
  • Constant springing calculation (see Figure 2)
  • Usage of a custom raycast function (see Figure 3)
  • Constant calling of bobbing calculation (see Figure 4)
  • Usage of constraints on client vs on server. Both under workspace.
  • Using Tween:Create():Play() instead of pre-creating tweens.
  • Using SetPrimaryPartCFrame vs using PrimaryPart.CFrame
  • Using LocalTransparencyModifier vs Transparency
  • Embedding tables instead of using a variable (see Figure 5)
  • Using Instance.new(_,parent) rather than Instance.new(_).Parent
  • Using FindFirstChild per heartbeat.
  • Puttings things like entire maps on client, vs on server.

Those are things I could think of, but if there are common mistakes that cause things to not be garbage collected I would love to hear. :slight_smile:

Figure 1
local wfc = game.WaitForChild
local ffc = game.FindFirstChild
local ffcoc = game.FindFirstChildOfClass
local getch = game.GetChildren

--// Services
local UserInput = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local TextService = game:GetService("TextService")
local Tween = game:GetService("TweenService")
local RepStore = game:GetService("ReplicatedStorage")
local Context = game:GetService("ContextActionService")

local events = wfc(RepStore,"Events")
local models = wfc(RepStore,"GunModels")
local modules = wfc(RepStore,"GunModules")
local assets = wfc(RepStore,"Assets")
local tools = wfc(RepStore,"Tools")

--// Variables
local cf = CFrame.new
local ang = CFrame.Angles
local v3 = Vector3.new
local lerpv3 = v3().lerp
local lerpcf = cf().lerp
local v2 = Vector2.new
local c3 = Color3.new
local rgb = Color3.fromRGB
local brc = BrickColor.new
local ud2 = UDim2.new
local sin,cos,tan = math.sin,math.cos,math.sin
local asin,acos,atan = math.asin,math.acos,math.atan
local pi,tpi,hpi = math.pi,math.pi*2,math.pi/2
local atan2 = math.atan2
local sqrt = math.sqrt
local abs = math.abs
local random = math.random
local floor,ceil,clamp = math.floor,math.ceil,math.clamp
local rad,deg = math.rad,math.deg
local rdm = math.random
Figure 2
-- This is a springing module, with springs being calculated a couple times every frame.
local spring = {}

do --// Spring
	local tick = tick
	local cos = math.cos
	local sin = math.sin
	local e = 2.718281828459045
	local function getposvel(s, d, p0, c1, c2, t0)
		local t = tick() - t0
		if d >= 1 then
			return (c1 + c2 * s * t) / e ^ (s * t) + p0, (c2 * s * (1 - t) - c1) / e ^ (s * t)
		else
			local h = (1 - d * d) ^ 0.5
			return (c1 * cos(s * h * t) + c2 * sin(s * h * t)) / e ^ (s * d * t) + p0, s * ((h * c2 - d * c1) * cos(s * h * t) - (h * c1 + d * c1) * sin(s * h * t)) / e ^ (s * d * t)
		end
	end
	function spring.new(initial, s, d)
		local self = {}
		local s = s or 15
		local d = d or 0.5
		local p0 = initial or 0
		local c1 = 0 * p0
		local c2 = 0 * p0
		local t0 = tick()
		function self:impulse(a)
			local p, v = getposvel(s, d, p0, c1, c2, t0)
			t0 = tick()
			c1 = p
			if d >= 1 then
				c2 = c1 + (v + a) / s
			else
				local h = (1 - d * d) ^ 0.5
				c2 = d / h * c1 + (v + a) / (s * h)
			end
		end
		local meta = {}
		function meta.__index(table, index)
			if index == "position" or index == "p" then
				local p, v = getposvel(s, d, p0, c1, c2, t0)
				return p
			elseif index == "velocity" or index == "v" then
				local p, v = getposvel(s, d, p0, c1, c2, t0)
				return v
			elseif index == "dampen" or index == "d" then
				return d
			elseif index == "speed" or index == "s" then
				return s
			end
		end
		function meta.__newindex(table, index, value)
			if index == "dampen" or index == "d" then
				d = value
			elseif index == "speed" or index == "s" then
				s = value
			elseif index == "target" or index == "t" then
				local p, v = getposvel(s, d, p0, c1, c2, t0)
				t0 = tick()
				p0 = value
				c1 = p - p0
				if d >= 1 then
					c2 = c1 + v / s
				else
					local h = (1 - d * d) ^ 0.5
					c2 = d / h * c1 + v / (s * h)
				end
			end
		end
		return setmetatable(self, meta)
	end
end

return spring
Figure 3
-- This is a raycast module. When a bullet is fired, this is called every single Heartbeat frame
local ray = {}
local ffc = game.FindFirstChild
local ffcoc = game.FindFirstChildOfClass
local cf = CFrame.new

do --// Ray
	ray.new = function(a, b, i)
		local d = (a - b).magnitude + 0.01
		local hit, pos, norm
		
		local function go(st, lv, i2)
			local ray = Ray.new(st, lv)
			local ig = i2
			local h, p, n = workspace:FindPartOnRayWithIgnoreList(ray, i2, false, true)
			if h then
				if h.Name == "bullet" or h.Name == "serverbullet" then 
					return
				end
				
				if ffcoc(h.Parent, "Humanoid") then
					hit, pos, norm = h, p, n
				elseif h.CanCollide == false or h.Name == "HumanoidRootPart" or h.Transparency == 1 then
					if ffc(h.Parent, "Humanoid") then
						hit, pos, norm = h, p, n
					else
						ig[#ig + 1] = h
						go(p, lv, ig)
					end
				else
					hit, pos, norm = h, p, n
				end
			else
				hit, pos, norm = h, p, n
			end
		end
		
		go(a, cf(a, b).lookVector * d, i)
		return hit, pos, norm
	end
end

return ray
Figure 4
-- Another function similar to this exists in the code, and both are called per frame.

local function cambob(infl)
	if infl == nil then
		infl = 1
	end
	
	local s0,d0,d1,d2,d3 = 10,12,15,29*40,14*40
	local t,s = tick()*infl,s0/2
	
	local c = cf(sin(t*s)/d0,cos(t*s0)/d1,0)
	local a = ang(sin(t*s0)/d2*infl^2,0,sin(t*s)/d3*infl^2)
	return c*a
end
Figure 5
bullet.Color = ({
			white = Color3.fromRGB(255,255,255);
			red = Color3.fromRGB(255,0,0);
			blue = Color3.fromRGB(0,80,255);
			purple = Color3.fromRGB(179,0,255);
			green = Color3.fromRGB(0,255,0);
			pink = Color3.fromRGB(255,0,255);
			black = Color3.fromRGB(0,0,0);
			yellow = Color3.fromRGB(255,255,0);
			orange = Color3.fromRGB(255,136,0);
			turquoise = Color3.fromRGB(0,255,255);
		})[player.stats.bcolors.Value] or Color3.new(1,1,0)

These issues are on front-page and semi-popular games, such as No-Scope Sniping and Infinite Autocorrect.

Thanks so much for your help!

23 Likes

This is very helpful for many developers and users asking how to remove or ask questions about lag, since most users on the platform experience it and hate it, especially me. :derp:

But I got a question: Wouldn’t you include another way lag is being brought onto the game such as models and too many voxels from parts and terrain that also coordinates with scripts?

1 Like

Oh I was asking if my specified things caused lag, sorry for the confusion.
When I can get confirmation, I will make a DevForum post in #resources.

1 Like

Alright, understood about what is going on here, also got confused a bit. :smile:

Off-topic: From the question I asked to add on, maybe some scripts fire and put in many parts into workspace, especially some error about cloning parts or objects. I’ve seen people ask about these problems in the forums. :flushed:

1 Like

It should be known that more instances = more lag. The question is, where should we draw the line?
There is actually quite a thick line between what’s too much and what is fine; devices and specs vary, and being inclusive of these platforms results in forced creativity for reduction of clone.

2 Likes

Try using GetPropertyChangedSignal instead of having while loops constantly looping and checking if the condition is met. (if you are using while loops)

2 Likes

Sometimes this may be caused by big models or a lot of particles like smoke, beams and other stuff

Also too many NPCs can cause this.
I made some research,so I don’t reccomend you to try anti-lag scripts because they do not work and they can’t stop process to stop lagging.

You may also be using some high graphic standards(like the things that you see in showcases and ultra realistic games).If your game don’t have to be super realistic try to use voxel lighting instead of ShadowMap,because this technology manage the shadow of every single part in the game and this may cause lag.

If you are sure that you don’t have any viruses try to check this things I mentioned here!

Try to check uou this links from the Roblox developer hub
[https://developer.roblox.com/en-us/articles/Improving-Performance ]
(Documentation - Roblox Creator Hub)

1 Like

• Always use coroutine over spawn or delay, as both of the latter use some form of wait() behind the scenes
• Weak tables would not cause lag, rather they’re a good way to ensure your tables let go of destroyed references to prevent memory leaks if used correctly
• Using ModuleScripts and working using a single script architecture is definitely the way to go when developing a game as long as you’re following good practices. The idea in general is good and shouldn’t cause lag, but the ultimate deciding factor is how you end up using them
• pcall recently just got a speed boost, but sometimes it’s usage is unavoidable. You should only use it sparingly with methods that are prone to failure, such as DataStoreService-related methods, or even getting a HumanoidDescriptionFromUserId
• The type checking annotations don’t end up in the bytecode, so you should be fine there (I annotate EVERYTHING in my modules to ensure I dont end up mismatching types, but that’s just me)
• The delay function should be avoided due to its use of wait() internally. You should instead find a way to bind the delayed body of code to an event (Humanoid.Changed:Wait() before the body, if that applies to your situation) or using a Heartbeat loop
• The length of your scripts aren’t really a problem unless all 800-2000 lines are executed EVERY FRAME. If possible, you should try separating your code into multiple modules if necessary to maintain readability, organization, and to have the code available in case another module would end up utilizing the same modules later
• Connections by themselves aren’t laggy, but if you’re constantly connecting events every frame and not disconnecting them shortly after, you’ll run into a totally unrelated problem regarding leaking memory
• What exactly is this for? If you’re serializing your tables for DataStores, then JSONEncode is the way to go. Sending data over remotes, that’s a whole different story. It all really depends on what you’re sending: some say that sending data as a string reduces the amount of data sent, but if you’re at the point where every byte matters, there’s probably a much larger underlying problem in how you’re managing your game
• Running spring calculations every frame should be fine on desktop systems, but depending on how heavy the load is, you may run into some lag and hot devices on mobile. The code looks well written, so I would assume your module wouldn’t cause too many problems
• I’m not sure if this would have much of an impact on performance, but I did spot something: Replacing cf(a,b).LookVector with (b-a).Unit, since that is how the LookVector is calculated and removes the need to construct a CFrame each frame
• The code looks pretty well written, but it seems like s0,d0,d1,d2,d3 are all constants that should be defined outside of the method. Other than that it should have negligible impact on performance
• It depends on what the constraints are managing, though in any case, you’d most likely want to use them on the server. The nearest client is given Network Ownership of the parts affected by the constraints (assuming the parts are affected by physics), meaning one client performs physics calculations for everyone else to see
• Caching created tweens and replaying them is definitely better, but if you’re not creating and playing tweens every frame you should be fine (I still sometimes end up using :Create():Play(), silly me)
• LocalTransparencyModifier is multiplied on top of the Transparency property of BaseParts; that’s mainly what the property is used for. Do keep in mind that all transparent parts are not subject to Instancing during render: each individual transparent part requires their own draw call, which can quickly add up in a game with thousands of transparent parts. I hear that unioning static transparent parts reduces the lag since the union would (theoretically) be drawn in one call, but I haven’t tested this yet
• Both methods are quite different. :SetPrimaryPartCFrame translates the entire model about the PrimaryPart, while PrimaryPart.CFrame would be editing the CFrame of just the PrimaryPart. In practice, it all really depends on what you’re going for, but the main difference is that one moves the entire model while the other only moves a single part UNLESS other parts are welded or constrained to it in some way
• Embedding a table in that way is NOT a good idea: the table is remade every time the enclosing function is called. You should definitely define the table outside of the method and reference it from within
• The Instance.new(Class,Parent) constructor isn’t inherently bad on its own; rather, it’s the way people use it. Setting the properties of a part BEFORE parenting it to workspace on the server is far more efficient than parenting it, THEN setting the properties. The latter only results in one replication call to clients for them to realize the part with all of its properties, while the latter could potentially result in a multitude of calls: one for realizing the part, then a bunch more for updating properties, those of which could be avoided by simply parenting the part last
• Using FindFirstChild on Heartbeat should be fine, as long as you’re not overusing the method for no apparent reason. Good practice would be to store the result of :FindFirstChild in a variable, then use that variable for your comparisons and calculations
• Having the map on the client would reduce stress on the server while increasing client load times, increasing both client and server memory usage, but with the benefit of fast map loading and no server-sided lag spikes. Having the map on the server would improve the client experience when joining and when playing on low-end devices, but the act of cloning the map from the server may take some time depending on the size of the map and how quickly that data can get sent to the client. In the end, both are perfectly valid methods of managing maps, but it really depends on your use case

27 Likes

Lag is caused by many factors and can never be eliminated on the developers end because it can be caused by things out of the developers control, for example poor network connection, the player’s device, etc. Apart from that the developer just needs to ensure that they monitor the performance of there scripts using the script performance window in studio. Also ensure that you don’t leak memory, and don’t run expensive code frequently, and never use RenderStepped unnecessarily. In addition, don’t let the server do all the work when some of the workload can be done on the client.

6 Likes

I rarely have loops. Most of my scripts solelu use events.

1 Like

I don’t have problems with models, just Lua. I have high untracked and Lua memory.

1 Like

None of the uses above you mentioned immediately translate to or cause lag. The efficiency and complexity of the script and its functions should define the performance of your code, not the API usage or lua methods you use.

Here's an example on how different approaches are more/ less efficient (run-time complexity wise)
-- Given two arrays, see if by using the first array you can create the second array. (Like a magazine to ransom note)

function solution_1 (magazine, note)
	for i=1, #note do
		local found_letter=false

		for j=1, #magazine do
			if magazine[j]==note[i] then
				found_letter=true
				magazine[j]='#'	-- Found letter, mark it used
				break
			end
		end

		if not found_letter then
			return false	-- Note cannot be made
		end
	end

	return true	-- Note can be made
end
-- Worst case complexity O(n * m), where n is the size of the note array, and m is the size of the magazine array


function solution_2 (magazine_note)
	alphabet={}

	for i=1, #magazine do
		alphabet[magazine[i]]= (alphabet[magazine[i]] or 0) + 1
	end

	for i=1, # note do
		if alphabet[note[i]] and alphabet[note[i]]>0 then
			alphabet[note[i]]=alphabet[note[i]]-1	-- Subtract quantity from magazine since we used one
		else
			return false	-- Note cannot be made
		end
	end
	return true	-- Completed, note can be made
end
-- Worst case complexity is O(max(m,n)), where n is the size of the note array, and m is the size of the magazine array
-- This one has better performance; imagine if both m,n = 100,000

Be smart about how you implement the code, instead of checking a condition every heartbeat see if you can make a model using event listeners and only check the condition when its relevant.

With memory management, understand lua’s garbage collector and avoid keeping table references after you’re done with them. Use local variables, instead of global variables. Disconnect used event listeners if they are not automatically disconnected.

Also know that everything in the roblox environment, workspace and such contributes to performance. Many objects = more calculation on the physics engine and rendering graphics. If many of these parts are variable to change, then more replication changes are queued.

1 Like

@wow13524 Thank you so much for the reply! It’s super long so I’m gussing it took a long time; I appreciate it and will use it to change how my code is written.

@greatgavin Your reply contradicts a bit with what wow13 said. Regardless, is there documentation on Lua/Roblox’s garbage collection?

1 Like

Same thing, just that wrap returns the function rather than a coroutine, but the point is to use coroutines if you don’t mind missing stack traces with the advantage of not having to wait for an open slot in the scheduler.

This has the same issue with waiting for an open slot unnecessarily just use some sort of Heartbeat based yield or something.

Rationale?

Using the second argument only becomes an issue when you’ve set required properties for the object beforehand, otherwise if it’s just to set the Parent property do it.

Putting them on the server will mean lesser network traffic because you won’t have to replicate too many things but then when you’ll need to, it might take more than expected just to set the Parent.

Just adding on, you don’t need to serialize tables before saving them to DataStores, at least under typical circumstances unless you want them to take twice the amount of characters since they’re JSONEncoded internally when saving them.

Don’t guess! Use the microprofiler or profile yourself!!!

Use ticks in code you suspect is slow and find how much time it took.

If you’re curious about any implementation at all then by all means wrap it like:

local start = tick()
-- Perform whatever
print('Executed calculation in:',tick()-start)

Regardless, the run-time complexity of your script is going to better define the performance.

For garbage collection, google ‘lua garbage collector’. But the jist is that it will collect dead objects automatically. Objects are considered dead when there are no references to it.

Here are some examples
-- Example 1
players=game:GetService('Players'):GetPlayers()	-- players is a reference to the table returned by GetPlayers()

for _, plr in pairs(players) do
	print(plr.Name)
end

-- players table remains in memory because the reference still exists, it might even be used in later code

some_signal.Event:Connect(function ()
	for _, plr in pairs(players) do
	    print(plr.Name)
    end
end)


-- Example 2
for _, plr in pairs(game:GetService('Players'):GetPlayers()) do		-- Performs same functionality with no references, the table returned by :GetPlayers will later be collected by garbage collector.
	print(plr.Name)
end


-- Example 3
function printPlayers()
	local players=game:GetService('Players'):GetPlayers()	-- players is a reference to the table returned by GetPlayers()

	for _, plr in pairs(players) do
		print(plr.Name)
	end
end					-- Function returns, since reference was a local variable it is removed from the stack when this function ends. So it no longer exists and the table will eventually be collected by garbage collector.
printPlayers()
2 Likes

Coroutines are not laggy, nor is spawn. Spawn is lower-frequency than coroutine since it runs on the Roblox scheduler, but to my experience neither of them are laggy.

Weak tables prevent lag. What they contain is to be garbage-collected when data isn’t apparent / accessable.

ModuleScripts are not laggy in the slightest. They’re just glorified functions.

PCall is not laggy, period.

Statically-typed Luau syntax I can’t say for.

Delay isn’t laggy.

Large scripts are bound to be laggier because they’re going to be running more compared to a smaller script. It’s kinda obvious.

Instance connections aren’t laggy.

Why would you ever use a JSON encoded string instead of decoding it? I can’t imagine a use case for that at all.

Seems to me like it’s laggy in excession.

You shouldn’t need to use a custom raycast function, nine times out of ten it’s probably going to be worse than the one Roblox has now.

See my explanation for Figure 2.

Constraints are bound to have a performance impact due to the physics calculations.

Tween:Create():Play() isn’t laggy. Creating the same tween over and over again and playing it however, is laggy.

SetPrimaryPartCFrame is worse than PrimaryPart.CFrame due to floating point errors, I believe. Correct me if I’m wrong.

I haven’t a clue what that property is.

Anonymous tables aren’t bad. If anything, binding them to a variable per use would be laggier due to the binding overhead.

Instance.new(, parent) isn’t the laggy thing, if memory serves correctly. What is laggy is doing procedures like repositioning or resizing while the object is visible within the workspace (or viewport, depends on how roblox renders).

Is this even a question? Of course that’ll be laggy.

You shouldn’t ever have to do that because server maps will always replicate to the client. The client would always have to render both.


Overall, everything is laggy in excession. Remember DRY. Have common sense. Run benchmarks. All that jazz. Generally, just be smart. :stuck_out_tongue:

3 Likes

I do want to note that my usage of JSON-Encoded tables are to store data in string values to be retireved by other scripts when that data is required, rather than having to rely on events. Many people ask about this, so that’s my reasoning.


@XxELECTROFUSIONxX I see many people debate coroutine vs. spawn. If you only had to use one, what would you pick?

@FieryEvent I’m not guessing, and I do regularly use tick().

@greatgavin In a sense, I should try to keep non-garbage-collectible things wrapped in a function or something so that they do get GC’d?

@Fifkee I notice that you and others take side to the server for using things like models and contraints. It seems that this makes replication better and reduces ping, correct?

You are all helpful, thanks so much!

1 Like

Garbage collection works automatically, so that if you don’t have references to an object it will be collected at some point. The third example I gave was to contrast the first, where local variables should be used over global variables. Function wrapping doesn’t help, local variables do. In the end, you really shouldn’t worry to much about how garbage collection works.

But you can always keep track of how much memory is in use with: print(collectgarbage('count'), 'Kilobytes')

2 Likes

Would you mind providing examples for each case? That is, code that you deem laggy, profiling it, with tick() for example, showing us the output, optimizing and using an alternative, profiling it again, and compare the results, also providing everything so we and roblox can reproduce (example). I don’t usually follow rules of thumb like these, because the engine is subjective to changes that may reaffirm or invalidate some cases. Also because you could have another worse case somewhere else that is dropping your FPS below 60, but you still keep optimizing the wrong code!