How can I optimize my code?

I’ve been making a footstep module, and I need to know how I can optimize my code to allow more efficiency and performance. I’m mainly asking this since I’m newer to coding and have ~1 year of actual programming experience/learning, but I’ve never truly learned to optimize my code.

In this particular case, I’m worried about this client-sided block of code that tracks the conditions for the newStep() function to fire. It worries me since it uses a lot of built-in APIs and if statements to track the conditions.

runS.PreRender:Connect(function(dt)
	if hum.FloorMaterial ~= Enum.Material.Air then
		if hum.Sit == false then
			if getVel() > 1 then
				tickStep = 1/(getVel()*.15)
				timeElapsed += dt
				if timeElapsed >= tickStep then
					newStep()
					timeElapsed -= tickStep
				end
			end
		end
	end
end)

Current Memory Usage:
Screenshot 2026-01-10 162518

2 Likes

To begin, consider combining your if-statements.

The first 3 if-statements could be combined into

if hum.FloorMaterial ~= Enum.Material.Air and not hum.Sit and getVel() > 1 then
    -- ...
end

I also heavily advise you to learn where it’s appropriate to flip the condition & turn the if-statement into an early return, for example like this:

if not (hum.FloorMaterial ~= Enum.Material.Air and not hum.Sit and getVel() > 1) then
    return
end

-- ...

… or

if hum.FloorMaterial == Enum.Material.Air or hum.Sit or getVel() <= 1 then
    return
end

-- ...

This isn’t really “optimization”, but it certainly does help you avoid insanely large indentation pyramids. (those are called guard clauses)

Then, it depends on how complex the getVel() function is to conclude whether it’s even worth it, but you could reuse its return value as long as it’s possible (unless it is always expected to return different values):

if hum.FloorMaterial == Enum.Material.Air or hum.Sit then
    return
end

local vel = getVel()
if vel <= 1 then
    return
end

timeStep = 1 / (vel * 0.15)
timeElapsed += dt

if timeElapsed >= tickStep then
    newStep()
    timeElapsed -= tickStep
end

Other than that, I can’t really point anything out - it looks fine to me. I can’t really say much more because I don’t know what any of the other functions do.

2 Likes

why use pre render..? use heartbeat, it’ll run less amount of times. What is in getVel and newStep function?
memory usage doesn’t have almost anything to do with code efficiency

But Heartbeat & PreRender are fired at the exact same frequency? More precisely, once per frame? The only difference is the order they’re fired in.

I’d update indentation:

runS.PreRender:Connect(function(dt)
	if hum.FloorMaterial == Enum.Material.Air then return end
	if hum.Sit then return end
	if getVel() <= 1 then return end

	tickStep = 1/(getVel()*.15)
	timeElapsed += dt

	if timeElapsed < tickStep  then return end

	newStep()
	timeElapsed -= tickStep
end)
2 Likes
runS.Heartbeat:ConnectParallel(function(dt) -- if this is all you're doing in this function then run it in parallel, ( runs on a different thread )
	if hum.FloorMaterial ~= Enum.Material.Air and -- make code more readable
		hum.Sit == false  and 
		getVel() > 1
	then
		tickStep = 1/(getVel()*.15)
		timeElapsed += dt
		if timeElapsed >= tickStep then
			newStep() -- This function is probably the reason for bad performance, if this function errors then don't use parallel
			timeElapsed -= tickStep
		end
	end
end) 

How the fuck did you use that much memory; it’s your functions, please share your functions

Note that a large portion of the bulk of the processing is in a module script, so let me know and I will show you the parts you need from the module

function newStep()
	local currentMat = mod.GetMaterial(hum)

	local sound = mod.RandomSound(currentMat)
	
	local newPlayer = char.feetPart["AudioPlayer" .. tostring(int)]
	print(newPlayer)
	int = (int%3)+1
	
	local audioPlayer: AudioPlayer = newPlayer
	audioPlayer.Asset = sound
	
	audioPlayer.TimePosition = 0
	audioPlayer:Play()
end

function getVel()
	return humRoot.AssemblyLinearVelocity.Magnitude
end
1 Like

lolz if you want micro optimizations and some nitpicks, sure

local AIR_MATERIAL = Enum.Material.Air
local PreRender = runS.PreRender
local magicnumber = 0.15 -- change the name if you want.

PreRender:Connect(function(dt)
    if hum.FloorMaterial == AIR_MATERIAL then return end
    if hum.Sit then return end

    local vel = getVel()

    if vel <= 1 then return end
    
    tickStep = 1 / (vel * magicnumber)
    timeElapsed += dt

    if timeElapsed < tickStep then return end

    newStep()
    timeElapsed -= tickStep
end)

the high memory usage is likely from being in roblox studio, join an actual game and see.

and tbh, you could probably replace the entire thing with Animation Events and GetMarkerReachedSignal()

1 Like

Hearbeat runs after physics simulation which is locked at 20 hertz

footstep..?
you know you can just add markers to walk and run animations?

you can just use a while do loop and calculate the amount of time to wait in task.wait

Have you done any research before posting this? The client, by default, runs at 60 FPS. Are you proposing that the physics simulation updates approximately every third frame? That’s just wrong, in fact the physics engine can simulate physics at 240Hz: source. You might be confusing the physics replication frequency with the simulation frequency?

With what I wrote above in mind, Heartbeat absolutely does NOT run at 20Hz.

local RunService = game:GetService("RunService")

local totalTime = 0
local n = 120

for _ = 1, n do
	totalTime += 1 / RunService.Heartbeat:Wait()
end

print("Average FPS:", totalTime / n)

*correction: it’s not delta time, but the estimated frame count

1 Like

that’s the dumbest thing i’ve heard so far this year

Nevermind, this is the dumbest thing i’ve heard so far this year

yeah my bad
[spoiler] [/spoiler]
[spoiler] [/spoiler]

Even faster

	
	task.desynchronize() -- run on a different thread
	
	if hum.FloorMaterial == Enum.Material.Air then return end
	if hum.Sit == true then return end
	
	local VelocityValue = GetVel() -- This shit probably math
	
	if VelocityValue <= 1 then return end
	
	tickStep = 1/(VelocityValue*.15)
	timeElapsed += dt
	
	if timeElapsed < tickStep then return end
	
	task.synchronize() -- go back to main thread
	
	newStep() 
	timeElapsed -= tickStep
	
end)

using task.desynchronize will ruin perfomance, it requires HUGE and EXPENSIVE math to be more efficient with it

1 Like

true, it’s not as efficient compared to big operations but it saves a couple microseconds in my testing

game["Run Service"].Heartbeat:Connect(function() -- Averages 7.4 ~ 7.6 ms
	local V = game.Players.LocalPlayer.Character
	V:MoveTo(Vector3.new(0,0,0))
end)

game["Run Service"].Heartbeat:Connect(function() -- averages 7.3 ~ 7.5 ms
	task.desynchronize()
	local V = game.Players.LocalPlayer.Character
	task.synchronize()
	V:MoveTo(Vector3.new(0,0,0))
end)

While it’s interesting that your benchmarks show a slight improvement, there are a few technical reasons why using task.desynchronize() for a single variable assignment is generally considered counter-productive in Roblox development.

Here is a breakdown of why you are seeing those numbers and why this pattern might actually hurt performance in a real game environment.

1. The Overhead of Context Switching

task.desynchronize() and task.synchronize() are not “free.”

  • Desynchronize: The engine must pause the execution of your script, package the current state, and move it to a worker thread.
  • Synchronize: The engine must wait for the “Parallel Phase” of the frame to finish, then move your script back to the “Serial Phase” (Main Thread) to execute MoveTo.

The time it takes to move between threads is often significantly longer than the time it takes to fetch the LocalPlayer.Character. By doing this, you are asking the CPU to do more “management” work to save a “micro-operation.”

2. The “MoveTo” Bottleneck

The most expensive part of your code is V:MoveTo(). Because MoveTo changes the physics state of the world, it cannot run in parallel.

In your second snippet, you:

  1. Switch to a worker thread.
  2. Get a reference to the character (extremely fast).
  3. Wait for the engine to sync back to the main thread.
  4. Run the heavy MoveTo operation.

The slight “gain” you see in your testing is likely a measurement artifact. When you desynchronize, the script’s execution is split across different phases of the frame. Your profiler might be recording the time spent in the Serial phase only, ignoring the time the script spent “waiting” in the parallel pool.

3. Parallel Luau’s “Sweet Spot”

Parallel Luau is designed for heavy computation. To see real benefits, you generally need to be doing something that takes a lot of CPU cycles, such as:

  • Pathfinding calculations for 100+ NPCs.
  • Complex math (Fractals, procedural terrain generation).
  • Raycasting hundreds of times per frame.

Using it to fetch a variable is like driving a semi-truck to the mailbox at the end of your driveway; the startup time of the engine takes longer than just walking there.

4. Why your testing might show it’s “faster”

If you are using os.clock() or the MicroProfiler to measure this, the desynced version might appear faster because you are essentially deferring work.

  • In Snippet 1, the script runs start-to-finish in one go.
  • In Snippet 2, the script runs a tiny bit, “yields” to the parallel phase, and finishes later.

The “Total Frame Time” (the most important metric for FPS) will almost certainly be higher in Snippet 2 because of the synchronization overhead.

Recommendation

If you want to optimize this specific code:

  1. Cache the Character: Don’t look up game.Players.LocalPlayer.Character every heartbeat. Define it once outside the connection and update it only when CharacterAdded fires.
  2. Use CFrame/PivotTo: MoveTo is an old method that performs extra checks (like making sure the character doesn’t get stuck in the floor). Using V:PivotTo() or setting V.PrimaryPart.CFrame is generally faster and more predictable.

Optimized Version:

local player = game.Players.LocalPlayer
local char = player.Character or player.CharacterAdded:Wait()

player.CharacterAdded:Connect(function(newChar)
    char = newChar
end)

game["Run Service"].Heartbeat:Connect(function()
    if char and char.PrimaryPart then
        char:PivotTo(CFrame.new(0, 0, 0))
    end
end)

This will likely outperform the desynchronized version without the thread-switching overhead!

are we deadass :skull_and_crossbones:
Someone, take his Programmer role.
Comparing multithreading with __index is so stupid😔

1 Like

I’ve been trying this recently and I can’t figure out how to properly get the AnimationTrack to call the :GetMarkerReachedSignal() event.

function getTrack()
	for i, v in pairs(animator:GetPlayingAnimationTracks()) do
		print(v)
		if v.Animation.AnimationId == walkAnim then --walkAnim = an rbxassetid
			v:GetMarkerReachedSignal("Step"):Connect(function()
				print("step")
				newStep()
			end)
		end
	end
end

getTrack()