Measure a Luau Loop Before Calling It “Optimized”
Run two equivalent loops through a repeatable timing harness, compare median measurements and understand why a microbenchmark cannot establish a whole-game FPS gain.

A shorter loop isn’t necessarily a faster one. Before calling it optimized, give both versions the same work and measure them. Otherwise you may be comparing different results, or just different code styles.
The harness below runs in your own Studio project and prints its measurements. It doesn’t assume which candidate will win; the timings depend on the machine, runtime and conditions of your run.
Define a question small enough to answer
We’ll compare numeric indexing with ipairs for one dense array and repeated addition. There are no network requests, instance lookups or print calls inside the timed loops. That keeps the question small enough to answer.
Each pass adds 1 through 1000, which totals 500500. A hundred passes should return 50050000. That arithmetic gives the harness a correctness check before we start interpreting speed.
Set up a clean run
Add a Script under ServerScriptService, set RunContext to Server and disable unrelated lesson scripts. Note the Studio version, date, device and context alongside the output.
Keep the setup identical for both candidates. A server run on a desktop and a client run on another device would change far more than the loop. Repeat in the context your actual code uses before making a project decision.
Run the complete harness
local values = table.create(1000)
for i = 1, 1000 do
values[i] = i
end
local passes = 100
local expectedTotal = 500500 * passes
local function indexed()
local total = 0
for _ = 1, passes do
for i = 1, #values do
total += values[i]
end
end
return total
end
local function iterated()
local total = 0
for _ = 1, passes do
for _, value in ipairs(values) do
total += value
end
end
return total
end
local function measure(fn, samples)
local started = os.clock()
local result = fn()
local elapsed = os.clock() - started
assert(result == expectedTotal, "Candidates must do equal work")
table.insert(samples, elapsed)
end
local function median(samples)
local ordered = table.clone(samples)
table.sort(ordered)
return ordered[math.ceil(#ordered / 2)]
end
assert(indexed() == expectedTotal)
assert(iterated() == expectedTotal)
local indexedSamples = {}
local iteratedSamples = {}
for round = 1, 9 do
if round % 2 == 0 then
measure(indexed, indexedSamples)
measure(iterated, iteratedSamples)
else
measure(iterated, iteratedSamples)
measure(indexed, indexedSamples)
end
task.wait()
end
print(string.format("Indexed median: %.4f ms", median(indexedSamples) * 1000))
print(string.format("ipairs median: %.4f ms", median(iteratedSamples) * 1000))
print("Expected total:", expectedTotal)Both functions get a warm-up call. The recorded rounds alternate their order, and the code yields between pairs. Assertions and print calls sit outside the measured interval so they don’t become part of one candidate’s workload.
That helps, but it won’t remove all noise. The simulation and other background work can still affect a sample.
Read the measurements without picking a favorite
Save both medians with the environment details. If they’re close, try fresh sessions before deciding one has a repeatable advantage. Keep individual samples if you extend the harness for a closer look.
Don’t select only the fastest run from a long series. Use the same summary rule for both functions, chosen before you see the numbers.
If the interval is too short to distinguish from noise, increase the workload moderately for both candidates and rerun. Avoid a huge loop that locks up your editing session.
Distinguish throughput from frame behavior
This measures a batch of computation, not a whole frame. Rendering, physics or another script may dominate the game, leaving this loop too small to affect the FPS a player sees.
Use the Roblox MicroProfiler to find frame-level work and spikes. It helps answer whether this is the part of the program worth optimizing.
Scheduling is another question. A large synchronous batch can delay other work even when its throughput looks good. If you split work across frames, measure that design separately rather than changing it for only one candidate.
Write a defensible conclusion
When sharing the result, include the workload, context, runtime, sample count and summary method. Say whether the difference was clear or inconsistent, rather than turning one small benchmark into a rule for every Luau program.
No clear winner is a useful result too. You may decide to keep the easier loop to read and spend your time on the bottleneck the profiler actually found.
Primary references
- os library reference — timing function used by the harness.
- MicroProfiler — investigate frame-level work.
- Improving performance — computation and scheduling considerations.



Comments 0
No approved comments yet. Start the discussion.