Threads and performance
Write FiveM scripts that don't eat frames: CreateThread and Wait, dynamic waits, distance checks, ox_lib points, resmon, the profiler and reading its results.
Diese Doku ist vorerst auf Englisch.
Every client script shares the same game frame with GTA itself and every other resource. A resource that uses 1 ms per frame on a 60 FPS client is taking 6% of the frame budget. Ten of those and players feel it. This page shows how loops work, the patterns that keep them cheap, and how to measure.
Threads and Wait
Lua in FiveM uses coroutines. CreateThread starts one, and Wait(ms) pauses it and gives control back to the game:
CreateThread(function()
while true do
-- do something
Wait(1000) -- sleep one second
end
end)Wait(0)means “run again next frame”. Needed for things drawn or checked every frame (markers, text, disabling controls).- A
while trueloop without aWaitfreezes the game (client) or the server (server side). txAdmin will eventually restart a server stuck like this. Waitonly works inside a thread or an event/command handler, which already run as coroutines.SetTimeout(ms, fn)runs a function once after a delay, without a loop.
The classic mistake
CreateThread(function()
while true do
Wait(0)
local pos = GetEntityCoords(PlayerPedId())
for _, shop in ipairs(Config.Shops) do -- 40 shops
if #(pos - shop.coords) < 20.0 then
DrawMarker(2, shop.coords.x, shop.coords.y, shop.coords.z, 0,0,0, 0,0,0, 0.3,0.3,0.3, 255,255,255,150, false,true,2, nil,nil,false)
end
end
end
end)It checks 40 distances every frame even when you’re on the other side of the map. Multiply by every resource written like this.
Pattern 1: dynamic wait
Sleep long when nothing is near, go per frame only when needed:
CreateThread(function()
while true do
local sleep = 1000
local pos = GetEntityCoords(PlayerPedId())
for _, shop in ipairs(Config.Shops) do
local dist = #(pos - shop.coords)
if dist < 20.0 then
sleep = 0
DrawMarker(2, shop.coords.x, shop.coords.y, shop.coords.z, 0,0,0, 0,0,0, 0.3,0.3,0.3, 255,255,255,150, false,true,2, nil,nil,false)
if dist < 1.5 and IsControlJustPressed(0, 38) then -- E
openShop(shop)
end
end
end
Wait(sleep)
end
end)Far from every shop, the loop runs once a second. That’s usually a 20 to 50x cut in cost.
Pattern 2: ox_lib points and zones
ox_lib does the distance bookkeeping for you and only calls your code near the point:
for _, shop in ipairs(Config.Shops) do
lib.points.new({
coords = shop.coords,
distance = 20,
nearby = function(self)
-- runs every frame only while within 20 units
DrawMarker(2, self.coords.x, self.coords.y, self.coords.z, 0,0,0, 0,0,0, 0.3,0.3,0.3, 255,255,255,150, false,true,2, nil,nil,false)
if self.currentDistance < 1.5 and IsControlJustPressed(0, 38) then
openShop(shop)
end
end,
})
endlib.zones does the same for boxes, spheres and polygons with onEnter / onExit. Interaction resources like ox_target avoid loops entirely: players aim at something and pick an option.
Pattern 3: events instead of polling
Don’t check “is the player in a vehicle?” every frame if you can react to a change. ox_lib’s cache updates values and fires events when they change:
lib.onCache('vehicle', function(vehicle)
if vehicle then
print('Entered vehicle', vehicle)
else
print('Left vehicle')
end
end)On the server, react to playerDropped, onResourceStop, state bag change handlers (see State bags) instead of loops.
More habits that matter
- Cache what doesn’t change within a loop: call
PlayerPedId()once per iteration, not five times. (ox_lib’scache.pedkeeps it for you.) - Use vector math:
#(a - b)is faster and simpler thanGetDistanceBetweenCoords. - Don’t create entities, blips or threads in loops without cleaning them up.
- Avoid string building and table creation every frame: they create garbage for the Lua GC.
- Server side, never block: use async database calls (
MySQL.querywith a callback, or.awaitinside a thread), not busy waits. - Don’t send events every frame. Send on change, or at most a few times per second, and batch.
- Unload what you load: models, anim dicts, ptfx assets, scaleforms.
Measuring: resmon
Press F8 and type:
resmon 1The resource monitor shows each client resource’s CPU time in ms per frame and memory. Rough guide:
| ms | Meaning |
|---|---|
0.00 to 0.02 |
Idle, great. |
0.02 to 0.10 |
Fine for most resources. |
0.10 to 0.50 |
Worth a look, especially if it’s constant while nothing happens. |
> 0.50 constant |
Something runs every frame that shouldn’t. |
Test in the situations that matter: standing still, driving, near the resource’s zone, with the UI open. A HUD or a target system will naturally use a bit more. On the server, txAdmin’s performance chart shows tick times, and the server console prints hitch warnings when a tick takes too long.
Measuring: the profiler
For “which function exactly?”, use the built in profiler. It works on the client (F8) and the server console:
profiler record 500
profiler viewprofiler record 500records 500 frames (the docs recommend 500).profiler statusshows whether it’s still recording.profiler viewopens the result in Chrome’s performance view (on the server, it prints a link you open yourself).profiler saveJSON myprofile.jsonsaves it to a file.
In the view, the green graph is FPS and the yellow one CPU time. Spikes are hitches. Hover a frame to see which resource and which line took the time.
To skip reading flame charts, drop the saved JSON into the Profiler Analyzer: it shows which resources and threads use your frame or tick time and suggests fixes.
Server performance
- Tick time: the server runs its main loop on one thread. Long running Lua on the server delays everything, including sync.
- Database: slow queries show up as
mysql_slow_query_warningmessages when you set that convar. Add indexes to columns you search by (identifier, citizenid, plate). - Entities: thousands of networked objects hurt everyone. Clean up what your scripts spawn.
- Scheduled restarts hide memory leaks but don’t fix them. Find them with resmon’s memory column and the profiler.
Assets are performance too
A perfectly coded server still stutters if it streams 4K textures on every car. Oversized .ytd files cause texture loss and memory pressure. See Streaming custom assets and the YTD Optimizer.
Next: State bags and OneSync.
