Strange Lag Caused by Calling :Destroy()

I am making a system to pick up or harvest items to add them into an inventory. I want the system to run without lag, which I was not expecting to have to deal with. It seems to be caused by calling :Destroy().

The first time I pick up an item in a playtest, the server lags fairly consistently on the 3-4th and 34-35th frame after calling :Destroy() on the script’s parent and the script itself.

Here’s a video:

The script output runs every frame and is the time passed between each RunService.Hearbeat:Wait(). This output is from another playtest:

Output

15:12:19.313 Destroyed - Studio
15:12:19.313 1 : 0.0004653000505641103 - Studio
15:12:19.313 nil - Studio
15:12:19.329 2 : 0.015778600005432963 - Studio
15:12:19.329 nil - Studio
15:12:19.346 3 : 0.016781099955551326 - Studio
15:12:19.346 nil - Studio
15:12:19.643 4 : 0.2972885000053793 - Studio
15:12:19.643 nil - Studio
15:12:19.655 5 : 0.011784399976022542 - Studio
15:12:19.655 nil - Studio
15:12:19.663 6 : 0.008206300088204443 - Studio
15:12:19.664 nil - Studio
15:12:19.678 7 : 0.015434199944138527 - Studio
15:12:19.679 nil - Studio
15:12:19.695 8 : 0.016785999992862344 - Studio
15:12:19.695 nil - Studio
15:12:19.712 9 : 0.0171703000087291 - Studio
15:12:19.713 nil - Studio
15:12:19.728 10 : 0.015445300028659403 - Studio
15:12:19.728 nil - Studio
15:12:19.747 11 : 0.018666299991309643 - Studio
15:12:19.747 nil - Studio
15:12:19.762 12 : 0.015251799952238798 - Studio
15:12:19.762 nil - Studio
15:12:19.779 13 : 0.01716130005661398 - Studio
15:12:19.779 nil - Studio
15:12:19.795 14 : 0.01576999993994832 - Studio
15:12:19.795 nil - Studio
15:12:19.812 15 : 0.017185600008815527 - Studio
15:12:19.812 nil - Studio
15:12:19.828 16 : 0.015714699984528124 - Studio
15:12:19.828 nil - Studio
15:12:19.845 17 : 0.017381300101988018 - Studio
15:12:19.845 nil - Studio
15:12:19.862 18 : 0.016752799972891808 - Studio
15:12:19.862 nil - Studio
15:12:20.159 19 : 0.29618359997402877 - Studio
15:12:20.159 nil - Studio
15:12:20.166 20 : 0.007649100036360323 - Studio
15:12:20.166 nil - Studio
15:12:20.180 21 : 0.014091299963183701 - Studio
15:12:20.180 nil - Studio
15:12:20.195 22 : 0.015474600018933415 - Studio
15:12:20.195 nil - Studio
15:12:20.212 23 : 0.016644999966956675 - Studio
15:12:20.212 nil - Studio
15:12:20.229 24 : 0.017265900038182735 - Studio
15:12:20.229 nil - Studio
15:12:20.246 25 : 0.016572199994698167 - Studio
15:12:20.246 nil - Studio
15:12:20.263 26 : 0.0177854000357911 - Studio
15:12:20.264 nil - Studio
15:12:20.279 27 : 0.015309599926695228 - Studio
15:12:20.279 nil - Studio
15:12:20.301 28 : 0.021857100073248148 - Studio
15:12:20.301 nil - Studio
15:12:20.316 29 : 0.015378799987956882 - Studio
15:12:20.316 nil - Studio
15:12:20.329 30 : 0.013008299982175231 - Studio
15:12:20.329 nil - Studio
15:12:20.349 31 : 0.019523499999195337 - Studio
15:12:20.349 nil - Studio
15:12:20.362 32 : 0.013953399960882962 - Studio
15:12:20.363 nil - Studio
15:12:20.379 33 : 0.016509099979884923 - Studio
15:12:20.379 nil - Studio
15:12:20.396 34 : 0.017109000007621944 - Studio
15:12:20.397 nil - Studio
15:12:20.414 35 : 0.01808429998345673 - Studio
15:12:20.415 nil - Studio
15:12:20.428 36 : 0.014261500095017254 - Studio
15:12:20.429 nil - Studio
15:12:20.445 37 : 0.016366799944080412 - Studio
15:12:20.445 nil - Studio

The time passed for frame 4 and 19 is higher than average, which is when the game lags. It’s a server side issue, not a client side one. When playtesting with the roblox player, framerate doesn’t drop, but if you’re walking during those frames you teleport back to where you were (since the server is lagging, causing a client-server difference).

After commenting out different parts of code, I have discovered that calling :Destroy() is lagging my game. When I comment that line of code out, the lag goes away. However, this is undesirable since, well, when you pick up an item you don’t want it to stay around! I tried finding workarounds, but using :Remove() or game.debris causes the same lag. I could theoretically set the part’s CanCollide to false and Transparency to 1, but that feels like a cheap fix and would probably lead to a lot of trash in the workspace.

My code is currently a script calling a ModuleScript. I’ve put the script and the relevant part of the ModuleScript here. The ModuleScript calls a few other “item” functions inside itself, but those are not causing the lag since when they’re commented out, the lag persists. I can post them if they would be helpful.

ModuleScript
-- the lag causing part is at the bottom of this function
function module.registerHarvestable(callingScript : Script, harvestable : Part, harvestableType : string, harvestTime : number, itemToGive : item, itemCount : number, durability : number, onDestroy)
	if not durability then
		durability = 1
	end
	
	if not onDestroy then
		onDestroy = function() return nil end
	end
	-- make the proximityPrompt
	local harvestPrompt = harvestDefaultProximityPrompt:Clone()
	harvestPrompt.ObjectText = itemToGive.itemName
	
	local actionAnimation
	if harvestableType == module.itemEnum.harvestableTypes.axeNecessary then
		harvestPrompt.ActionText = "Chop"
		actionAnimation = script.MiningAnimation
	elseif harvestableType == module.itemEnum.harvestableTypes.pickaxeNecessary then
		harvestPrompt.ActionText = "Mine"
		actionAnimation = script.MiningAnimation
	elseif harvestableType == module.itemEnum.harvestableTypes.shovelNecessary then
		harvestPrompt.ActionText = "Dig"
		warn("There is no dig animation yet!")
	elseif harvestableType == module.itemEnum.harvestableTypes.instant then
		harvestPrompt.ActionText = "Pick up"
		actionAnimation = script.PickupAnimation
		harvestTime = 0
	end
	harvestPrompt.Parent =  harvestable
	
	local destroyHarvestable = false
	
	-- when a player clicks we begin the collecting animation, and if the player doesn't cancel it they harvest the item!
	local harvestConnection = harvestPrompt.Triggered:Connect(function(playerWhoTriggered: Player) 
		
		local harvestSuccess = false
		-- get the player to move to the item to harvest
		local character = playerWhoTriggered.Character
		local humanoid : Humanoid = character:FindFirstChild("Humanoid")
		local hrp : Part = character:FindFirstChild("HumanoidRootPart")
		local animator : Animator = humanoid:FindFirstChild("Animator")
		
		local actionTrack = animator:LoadAnimation(actionAnimation)
		
		if harvestTime > 0 then
			startHarvestingAnimEvent:FireClient(playerWhoTriggered)
		end
		
		-- first we make the player actually look at the harvestable part
		
		local partToPlayer = Vector3.new(hrp.Position.X - harvestable.Position.X, 0, hrp.Position.Z - harvestable.Position.Z)
		partToPlayer = (partToPlayer / partToPlayer.Magnitude) * 3 -- 3 is the amount of studs away that we stand from the object.
		-- it may need to be customizable. just FYI
		
		-- we don't do this if we're picking it up instantly
		
		if harvestTime > 0 then
			
			-- move to. When finished, run the startHarvesting event so we start mining
			local finalVector = Vector3.new(partToPlayer.X + harvestable.Position.X, hrp.Position.Y, partToPlayer.Z + harvestable.Position.Z)
			humanoid:MoveTo(finalVector)
			wait()
			local timePassed = 0
			-- for some reason moveFinished was unreliable. IDK why. So I just made my own move finished
			repeat 
				timePassed += wait()
			until (hrp.Position - finalVector).Magnitude < 2.5 or timePassed > 5
			
		end
		hrp.CFrame = CFrame.new(hrp.Position, Vector3.new(harvestable.Position.X, hrp.Position.Y, harvestable.Position.Z))
		wait()
		-- start animating
		actionTrack:Play()
		
		local forceHarvestStop = false
		
		local playerStopConnection = stopHarvestingEvent.OnServerEvent:Connect(function(player: Player, ...: any) 
			if player == playerWhoTriggered then
				forceHarvestStop = true
				harvestSuccess = false
			end
		end)
		
		local timePassed = 0
		harvestSuccess = true
		while (not forceHarvestStop) and timePassed < harvestTime do
			timePassed += wait()
		end
		wait()
		
		-- again, we don't need to tell the client to stop or stop the animation if we're picking up the item instantly
		if harvestTime > 0 then
			stopHarvestingEvent:FireClient(playerWhoTriggered)
			actionTrack:Stop()
		end
		
		-- if success, great! we give the items
		if harvestSuccess then
			
			local inventory = playerWhoTriggered:FindFirstChild("Inventory")
			
			if inventory then
				for i = 1, itemCount, 1 do
					--local newItem : item = module.newItem(itemToGive)
					--local newValue = module.itemTypeToValue(newItem)
					--newValue.Parent = inventory
				end
			end
			
			durability -= 1
			if durability == 0 then
				harvestPrompt.Enabled = false
				task.spawn(function ()
					onDestroy()
					destroyHarvestable = true
				end)
			end
			
		end
	end)
	
	task.spawn(function()
		repeat
			wait()
		until destroyHarvestable
		print("Get Ready!")
		task.wait(5)
		harvestConnection:Disconnect()
		harvestable:Destroy() -- it's this line. Commenting this out resolves the lag.
		callingScript:Destroy() -- this line too. But if I comment this, the picked up part stays!
		local startTime = tick()
		game:GetService("RunService").Heartbeat:Wait()
		print(tick() - startTime)
	end)
	
end

I tried using task.spawn() so the destruction could be handled asynchronously, but it didn’t fix anything. That’s why the convoluted system for the part’s destruction exists (destroyHarvestable is set to true, which causes the thread’s loop to stop).

Script (really just a call to the ModuleScript)
local itemScript = require(game:GetService("ServerScriptService").ModuleScripts.ItemScript)
local rs = game:GetService("RunService")
itemScript.registerHarvestable(
	script,
	script.Parent,
	itemScript.itemEnum.harvestableTypes.instant,
	0,
	itemScript.getItemByName("Normal Stone"),
	2,
	1,
        -- this is a callback that runs just before the :Destroy() in the ModuleScript..
	function()
		script.Parent:Destroy() -- when commented, these also can resolve the lag. But I don't want that!
		script:Destroy() -- this line too.
		print("Destroyed")
		
		-- this code makes the heartbeat output.
		local t = os.clock()
		for i = 1, 37, 1 do
			rs.Heartbeat:Wait()
			local newT = os.clock()
			print(i, ':   ', newT - t)
			print(script.Parent)
			t = newT
		end
		
end)

I am not a very experienced scripter or developer, and I have no idea why there is lag at all, or even why :Destroy() is causing the lag. I’m even more puzzled that the lag happens slightly after calling :Destroy(), not immediately after. If anyone can help, thanks! I can invite you to collaborate in studio if that would help solve the problem.

2 Likes

I have no idea why this lag appears but try to not destroy them. Put them into replicated storage, and if they needs to be spawned place them in the workspace.

It works!

I decided to move it (the harvestable) to ServerStorage instead to avoid lagging clients. (I assume having too much stuff in ReplicatedStorage would lag, but I don’t actually know.)
I also tried destroying the part inside ServerStorage just to check, but the server still lagged.

This solution does work, but I’d still prefer to just get rid of the lag and destroy the part, because now I have a folder in ServerStorage full of garbage. It’s better than lag though, so thank you!

Try this creating all the items on the client and destroying them on the client.

Lag is caused by a task that takes longer than it should. In Roblox, all code naturally executes cooperatively, meaning only one thread of code executes any given time. Other code cannot run until the current code yields (pauses) or completes. A lot of the time, hardware is pushed to its limits and cannot deliver results in a timely manner, but elongated tasks can also be incited by software.

When it comes to destroying instances, so long as no additional references exist, the garbage collector will step in to officially clear the instance’s memory from the heap. This process can have an affect on performance if multiple cleanup tasks are piled up in quick succession. Instance:Destroy destroys the instance’s children as well, which in turn destroy their children and so on.

Ensure you’re not making any complex instances that can lead to a large destruction task, or destroying many instances in short time periods. You might have some inefficient code that responds to or proceeds the destruction of your instances

I tried it and it works! I destroyed the item without any lag.

I’d really rather not use the client, however. I think that creating grabbable items from the client could lead to exploits eventually, and could just lead to weird things happening (for instance, one player could cut down a tree but another player sees the tree as still standing). I think this would be better server side.

I bet it could be done, it just would take a lot of remoteEvents. I’d rather simply target the server-side lag. Thank you!

Currently, only two instances are being destroyed: a basic part and its script that calls the ModuleScript. That shouldn’t be a complex or large load, I don’t think. The only thing I would have responding to the destruction of the part would be the thread calling the ModuleScript, which would terminate.

These things are typically done on the client. The server keeps a record of the instances but only in spirit. What’s mostly preserved are positions and yields. When a client asks to perform an action and/or accept yields, the server can perform sanity checks. A FireAllClients call is made to instruct other clients to destroy the yields or replicate the action

It’s most likely due to inefficient software then; it doesn’t have to be yours, either. This is not the first case I’ve seen this past week, so maybe Roblox is to blame for some replication tinkering

I’ll keep in mind what you said about the client-server replication, I believe that will be useful.
I guess I’ll wait for the next few studio updates to see if the issue happened to be roblox itself, and I’ll move my harvesting scripts to the client side for this if it happens to be my problem. Thank you!

It’s better to keep them in the replicated storage. Delete scripts from them. Create only one script in ServerScriptService to control them. If somone will try to cheat in replicate storage they will not exploit this because controlling script will be on the server side.