I've made this entity system for a game I'm making, and it hold around 300-600 enemies before lagging anyone know how to optimize it more (the enemies are on client server sends client position to update it)

server:

local mob = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")
local LastUpdate = tick()
local UpdateDT = 0.1
local ClientInfo = {}
shared["Positions"] = {}
local MobsFolder =ReplicatedStorage:WaitForChild("Mobs")

local mysteryMob = {
	["Mystery"] = {MobsFolder:WaitForChild("Zombie"), MobsFolder:WaitForChild("Speedy"), MobsFolder:WaitForChild("Boss"), MobsFolder:WaitForChild("Spy")}
}

game:GetService("RunService").Heartbeat:Connect(function(DeltaTime)
	local map = workspace:WaitForChild("Map") 
	
	local success, errorMsg = pcall(function()
		for number, data in pairs(shared["Positions"]) do 
			if not data or not number or not data[3] then continue end			
			local CurrentWaypoint = map.Waypoints:FindFirstChild(data[1])
			local OldWaypoint = map.Waypoints:FindFirstChild(data[1]-1) or map.Start

			if not CurrentWaypoint or data[2].Y <= 0 then 
				if mysteryMob[data.Name] and CurrentWaypoint then
					mob.Mystery(data,  OldWaypoint.CFrame:Lerp(CurrentWaypoint.CFrame, data[3]),  data[3], data[1])
				end
				if not CurrentWaypoint then
					map.Base.Humanoid:TakeDamage(data[2].Y)
				end
				ClientInfo[number] = {Dead = true}
				Events.UpdatePosition:FireAllClients(ClientInfo)
				shared["Positions"][number] = nil
				shared["currentMobs"] -= 1

				continue
			end

			local distance = (OldWaypoint.Position - CurrentWaypoint.Position).Magnitude
			data[3] += DeltaTime * data[2].X/50 / distance
			if data[3] >= 1 then
				data[1] += 1
				data[3] = 0
			end
			ClientInfo[number] = {Vector2int16.new(math.floor(CurrentWaypoint.Position.X*50), math.floor(CurrentWaypoint.Position.Z*50)), data[2].Y}
		end
	end)
	
	if not success then
		warn(errorMsg)
	end
	if success and tick() - LastUpdate >= UpdateDT then
		LastUpdate = tick()
		Events.UpdatePosition:FireAllClients(ClientInfo)
	end
end)


local num = 0 
shared["currentMobs"] = 0
function mob.Spawn(name, quantity, map)
	local Exists = ReplicatedStorage.Mobs:FindFirstChild(name)

	if Exists then 
		for i = 1, quantity do 
			num += 1
			if shared["currentMobs"] < 0 then
				shared["currentMobs"] = 0
			end
			shared["currentMobs"] += 1
			shared["Positions"][num] = {1, Vector2int16.new(Exists.Config.Speed.Value*50, math.round(Exists.Config.Health.Value)), 0,}
			Events.Render:FireAllClients(name, num, false)
			task.wait(0.25)	
		end
	end
end

function mob.Mystery(mob, cframe, moved, waypoint)
	local mystery = ReplicatedStorage.Mobs.Mystery	
	
	if mysteryMob[tostring(mob.Name)] then
		local Table =  mysteryMob[tostring(mob.Name)]
		local newMob = Table[math.random(1, #Table)]
		local oldwaypoint = workspace.Map.Waypoints:FindFirstChild(waypoint-1) or workspace.Map.Start
		local Waypoint =  workspace.Map.Waypoints:FindFirstChild(waypoint)
		shared["currentMobs"] += 1
		shared["Positions"][num] = {waypoint, Vector2int16.new(newMob.Config.Speed.Value*50, math.round(newMob.Config.Health.Value)), moved,}
		print(shared["Positions"][num].Moved)
		Events.Render:FireAllClients(newMob.Name, num, false, cframe)
	end
end

return mob

Client:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")

local LastUpdate = tick()
local ud = 0.0085

shared["Enemies"] = {}
shared["Units"] = {}

local positions = {}
local UnitPositions = {}

local map = workspace:WaitForChild("Map")
local base = map:WaitForChild("Base")

local PS = game:GetService("PhysicsService")

local function animation(object, anim)
	Events.AnimateZombie:Fire(object, anim)
end

Events.UpdatePosition.OnClientEvent:Connect(function(info, unitInfo)
	if info then
		positions = info
	elseif unitInfo then
		UnitPositions = unitInfo
	end
end)


local function moveEnemy(deltaTime)
	for Enemyobject, data in pairs(shared["Enemies"]) do 
		if Enemyobject and positions[data] then 					
			local pos = positions[data]			
			local Paosition = pos[1]
			if not Paosition or not Paosition.X or not Paosition.Y then 
				shared["Enemies"] [Enemyobject] = nil
				Enemyobject:Destroy()
				continue
			end
			Enemyobject.Config.Health.Value = pos[2]
			local Position = Vector3.new(Paosition.X/50, Enemyobject.PrimaryPart.Position.Y, Paosition.Y/50)
			local Distance = (Enemyobject.PrimaryPart.Position - Position).Magnitude

			local speed = Enemyobject.Config.Speed.Value

			local cframe = CFrame.new(Paosition.X/50, Enemyobject.PrimaryPart.Position.Y, Paosition.Y/50)

			if Distance > 1 then
				Enemyobject.HumanoidRootPart.CFrame = CFrame.lookAt(	Enemyobject.HumanoidRootPart.Position, Position)
			end
			Enemyobject:PivotTo(Enemyobject.PrimaryPart.CFrame:Lerp(cframe, deltaTime*speed/Distance))		
		end
	end
end

local function moveUnit(deltaTime)
	for UnitObject, data in pairs(shared["Units"] ) do 
		if UnitObject and UnitPositions[data] then 					
			local pos = UnitPositions[data]			
			local Paosition = pos[1]
			if not Paosition or not Paosition.X or not Paosition.Y then 
				shared["Units"] [UnitObject] = nil
				UnitObject:Destroy()
				continue
			end
			UnitObject.Config.Health.Value = pos[2]
			local Position = Vector3.new(Paosition.X/50, UnitObject.PrimaryPart.Position.Y, Paosition.Y/50)
			local Distance = (UnitObject.PrimaryPart.Position - Position).Magnitude

			local speed = UnitObject.Config.Speed.Value

			local cframe = CFrame.new(Paosition.X/50, UnitObject.PrimaryPart.Position.Y, Paosition.Y/50)

			if Distance > 1 then
				UnitObject.HumanoidRootPart.CFrame = CFrame.lookAt(UnitObject.HumanoidRootPart.Position, Position)
			end
			UnitObject:PivotTo(UnitObject.PrimaryPart.CFrame:Lerp(cframe, deltaTime*speed/Distance))		
		end
	end
end

game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
	if tick() - LastUpdate < ud then 
		return
	end	
	LastUpdate = tick()
	moveUnit(deltaTime)
	moveEnemy(deltaTime)
end)

Events.Render.OnClientEvent:Connect(function(name, EnemyNumber, unit, cframe)
	if not unit then	
		local map = workspace:WaitForChild("Map")
		local start = map:WaitForChild("Start")

		local enemy = ReplicatedStorage.Mobs:FindFirstChild(name):Clone()
		enemy.Parent = workspace.Mobs
		if not enemy.PrimaryPart then
			enemy.PrimaryPart = enemy.HumanoidRootPart
		end
		enemy.PrimaryPart.Anchored = true
		enemy.PrimaryPart.CanCollide = false
		
		local zombieTag = Instance.new("BoolValue")
		zombieTag.Name = "ZombieTag"
		zombieTag.Parent = enemy
		for i, v in pairs(enemy:GetChildren()) do 
			if v:IsA("BasePart") then 
				PS:SetPartCollisionGroup(v, "Mob")
			end
		end
		enemy.PrimaryPart.CFrame = cframe or start.CFrame + Vector3.new(0, (enemy.PrimaryPart.Size.Y/2 - 1), 0)

		shared["Enemies"] [enemy] = EnemyNumber 
		animation(enemy, "Walk")
	else
		local map = workspace:WaitForChild("Map")
		local start = map:WaitForChild("End")

		local unit = ReplicatedStorage.Units:FindFirstChild(name):Clone()
		unit.Parent = workspace.Units
		if not unit.PrimaryPart then
			unit.PrimaryPart = unit.HumanoidRootPart
		end
		unit.PrimaryPart.Anchored = true
		unit.PrimaryPart.CanCollide = false

		local unitTag = Instance.new("BoolValue")
		unitTag.Name = "UnitTag"
		unitTag.Parent = unit
		for i, v in pairs(unit:GetChildren()) do 
			if v:IsA("BasePart") then 
				PS:SetPartCollisionGroup(v, "Unit")
			end
		end
		unit.PrimaryPart.CFrame = cframe or start.CFrame + Vector3.new(0, (unit.PrimaryPart.Size.Y/2 - 1), 0)

		shared["Units"] [unit] = EnemyNumber 
	end
end)