Every docs URL answers with Markdown when you add .md. Site map: llms.txt
Measure before you optimise
How to find the code that actually costs you: resmon, the FiveM profiler, tick times, and why the shape of a loop matters more than any single line.
Every performance argument in FiveM ends the same way: someone measures. Until then it is opinion. A resource that “feels heavy” is usually fine, and the one nobody suspects is the one waking up 60 times a second to loop over a config table.
Measure first, then use the rest of this section on whatever the numbers point at.
The two budgets
| Side | Budget | What eats it |
|---|---|---|
| Client | About 16 ms per frame at 60 FPS, shared by GTA and every resource | Per frame loops, drawing, distance checks, NUI |
| Server | One tick, shared by every resource and by sync | Blocking calls, slow queries, big loops, heavy event handlers |
Neither side has a spare thread waiting for you. Client Lua runs inside the game’s frame, and server Lua runs on the server’s main loop. Anything slow delays everything else.
resmon: which resource
Press F8 in game and run:
resmon 1You get one row per client resource with CPU time per frame and memory. Roughly:
| Time per frame | Reading |
|---|---|
0.00 to 0.02 ms |
Idle. Nothing to do here. |
0.02 to 0.10 ms |
Normal for a resource doing something. |
0.10 to 0.50 ms |
Look at it, especially if it stays there while nothing is happening. |
Over 0.50 ms, constantly |
Something runs every frame that should not. |
Two things people get wrong with resmon:
- Test in the right place. A shop resource costs nothing across the map and costs real time standing in the shop. Both numbers are interesting, and only one of them is the bug.
- Watch the idle number. A HUD that costs
0.30 mswhile drawing is doing its job. A fishing script that costs0.30 mswhile you stand in the desert is not.
On the server, txAdmin’s performance chart shows tick times, and the console prints hitch warnings when a tick runs long.
The profiler: which function
When resmon has named the resource, the built in profiler names the code. It works in the client console and the server console:
profiler record 500
profiler status
profiler view
profiler saveJSON myprofile.jsonprofiler record 500 records 500 frames or ticks, profiler view opens the result as a flame chart, and saveJSON writes it to a file in the server or game folder.
If you would rather not read flame charts, drop the saved JSON into the Profiler Analyzer. It ranks resources and threads by the time they used and points at the usual causes.
Read the shape, not the line
When you have the numbers, look at the structure before you look at the syntax. In rough order of what is worth fixing:
CreateThread(function()
while true do
Wait(0) -- 60 passes a second, forever
local ped = PlayerPedId() -- a native call, every frame
for i = 1, #Config.Shops do -- 40 shops, every frame
local shop = Config.Shops[i]
if #(GetEntityCoords(ped) - shop.coords) < 20.0 then
drawShop(shop)
end
end
end
end)The problem is not #(a - b) and it is not the for loop. The problem is that the whole block runs every frame whether or not a shop is anywhere near you, and that GetEntityCoords is called once per shop instead of once per pass.
CreateThread(function()
local shops = Config.Shops
while true do
local sleep = 1000
local pos = GetEntityCoords(PlayerPedId()) -- once per pass
for i = 1, #shops do
local shop = shops[i]
if #(pos - shop.coords) < 20.0 then
sleep = 0 -- only now go per frame
drawShop(shop)
end
end
Wait(sleep)
end
end)Same natives, same loop, same maths. Away from every shop it now runs once a second instead of 60 times, and it calls GetEntityCoords once instead of 40 times. That is the kind of change that shows up in resmon. Nothing else on these pages will beat it.
Threads and performance goes further with dynamic waits, ox_lib points and zones, and reacting to events instead of polling.
Where the small stuff belongs
The rest of this section has plenty of “this form is cheaper than that form”. They are real, and they are small. Use them like this:
- Writing new code: pick the cheap form. It costs you nothing to type
t[#t + 1] = vinstead oftable.insert(t, v). - Inside a hot loop: worth changing. A thousand iterations a frame turns a tiny difference into a visible one.
- In code that runs once at startup, or on a command: leave it alone. Readability wins.
Warning
Do not trust benchmark numbers from forum posts, including old ones about FiveM. The Lua runtime changed, the VM changed, and a lot of the numbers still going around were measured on versions nobody runs any more. If a claim matters to your resource, measure it on your resource.
Assets count too
A perfectly written resource still stutters if it streams 4K textures onto every car. Oversized .ytd files cause texture loss and memory pressure, and no amount of Lua tuning fixes that. See Streaming custom assets and the YTD Optimizer.
Next: Scope, files and layout.
