Help optimizing gun script

I’m making a gun script and it works completely fine, and it is smooth with the use of :SetNetworkOwner(), but the thing is when I try testing it shooting 30 shots a second, it starts lagging quite bad. Is this normal or is this something that isn’t optimized enough?

Here is the code:
Local Script

local down = false
local mouse
local db = 0
script.Parent.Equipped:Connect(function(m)
	mouse = m
	m.Button1Down:Connect(function()
		down = true
	end)
	m.Button1Up:Connect(function()
		down = false
	end)
end)
script.Parent.Unequipped:Connect(function()
	mouse = nil
	down = false
end)
game:GetService("RunService").RenderStepped:Connect(function(step)
	if mouse and down then
		for i = 1, 12 do -- to test shotgun, shooting 12 per bullet and 5 times a second for this test
			script.Parent.Shoot:FireServer(mouse.Hit.Position) -- remote event, see server script
		end
		db = 1 / 5
	end
	db -= step
end)

Server:

local plr = script.Parent.Parent.Parent -- messy ikr
local handle = script.Parent.Handle -- obviously a part

local main = coroutine.create(function(pos) -- Look lower for coroutine explanation, this is quite simple
	local function Fire() -- shooting function
		local start = plr.Character.Head.Position
		local cf = CFrame.lookAt(start, pos)
		
		local beam = Instance.new("Part")
		beam.Color = Color3.new(1, 0, 0)
		beam.Transparency = .7
		beam.Material = Enum.Material.Neon
		beam.Name = "Beam"
		beam.Size = Vector3.new(.1, .1, 4)
		beam.Locked = true
		beam.CanCollide = false
		-- cframe manipulation and stuffs
		beam.CFrame = cf * CFrame.new(0, 0, -beam.Size.Z / 2)
		beam.Velocity = cf.LookVector.Unit * 200
		-- activate bullet
		local force = Instance.new("BodyForce", beam)
		force.Force = Vector3.new(0, 0, 0)
		beam.Parent = workspace
		force.Force = Vector3.new(0, workspace.Gravity * beam:GetMass(), 0)
		beam:SetNetworkOwner(plr)
		-- add debris
		game.Debris:AddItem(beam, 5)
	end
	while 1 do
-- what this does is just run Fire() and wait until this coroutine is called again, then it redefines pos which is mouse.Hit.Position
		Fire()
		pos = coroutine.yield()
	end
end)
script.Parent.Shoot.OnServerEvent:Connect(function(player, pos)
	if player == plr then -- just a check i guess
		coroutine.resume(main, pos) -- run the coroutine and send mouse.Hit.Position
	end
end)

Here is a video with Summary and Micro-Profiler


Edit: Added vid and corrected transparency property

tl;dr I am having performance issues with a gun shooting 30+ times a second, is this normal?
Thanks.

Render stepped is very costly try using stepped or heartbeat instead

Heartbeat still gives me similar frames. Is there anything else I can try?

Just noticed that I didn’t have a check for the debounce so the gun must’ve been shooting 120 times a second with 12 shots, making a whopping 1440 bullets a second. I’m pretty sure it still lags a bit.

To add on to this, I still drop from 120 frames to 100 with 60 shots a second, and to 80-90 with 720 shots a second. For the prior, my maximum frame time was around 20-25ms, while the latter, with 12 times more shots, only went up to around 30 and sometimes 40.

Edit: Here is my ending code if anyone wants to look.

Local

local down = false
local mouse
local db = 0

script.Parent.Equipped:Connect(function(m)
	mouse = m
	m.Icon = "rbxassetid://6380417283"
	m.Button1Down:Connect(function()
		down = true
	end)
	m.Button1Up:Connect(function()
		down = false
	end)
end)
script.Parent.Unequipped:Connect(function()
	mouse = nil
	down = false
end)
game:GetService("RunService").RenderStepped:Connect(function(step)
	if mouse and down and db == 0 then
		for i = 1, 12 do -- this is 720 shots a second lol
			script.Parent.Shoot:FireServer(mouse.Hit.Position)
		end
		db = 1 / 60
	end
	db = math.max(db - step, 0)
end)

Server

local plr = script.Parent.Parent.Parent
local handle = script.Parent.Handle
local maxSpread = math.rad(4)

local function rand() -- For spread
	return math.random() * 2 - 1
end
local function unit(a, b) -- To make spread into a circle
	local mag = math.sqrt(a ^ 2 + b ^ 2)
	local mult = maxSpread / mag
	if mag > maxSpread then
		a *= mult
		b *= mult
	end
	return a, b
end

local main = coroutine.create(function(pos) -- Look at original post for coroutine info
	local function Fire()
		local start = plr.Character.Head.Position
		local xRand, yRand = unit(rand() * maxSpread, rand() * maxSpread) -- get random values
		local cf = CFrame.lookAt(start, pos) * CFrame.Angles(xRand, yRand, 0) -- do cframe stuff
		
        local beam = Instance.new("Part") -- I cloned this, pretty sure cloning an already-made part is more efficient than setting the properties in the script
		beam.Color = Color3.new(1, 0, 0)
		beam.Transparency = .7
		beam.Material = Enum.Material.Neon
		beam.Name = "Beam"
		beam.Size = Vector3.new(.1, .1, 4)
		beam.Locked = true
		beam.CanCollide = false
		
					
		local force = Instance.new("BodyForce", beam)
		force.Force = Vector3.new(0, 0, 0)
		beam.Parent = workspace
		force.Force = Vector3.new(0, workspace.Gravity * beam:GetMass(), 0)

		beam.CFrame = cf * CFrame.new(0, 0, -beam.Size.Z / 2) -- just stuff
		beam.Velocity = cf.LookVector.Unit * 200

		beam.Parent = workspace
		beam:SetNetworkOwner(plr)

		game.Debris:AddItem(beam, .8)
	end
	while 1 do
		Fire()
		pos = coroutine.yield() -- look at original post for more info
	end
end)

script.Parent.Shoot.OnServerEvent:Connect(function(player, pos) -- just a remote inside of tool
	if player == plr then
		coroutine.resume(main, pos)
	end
end)