İçeriğe geç
Threads and performance

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.

Bu dokümanlar şimdilik İngilizce.

Last updated

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.

Plays from youtube-nocookie.com after you click.

Threads and Wait

Lua in FiveM uses coroutines. CreateThread starts one, and Wait(ms) pauses it and gives control back to the game:

Lua
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 true loop without a Wait freezes the game (client) or the server (server side). txAdmin will eventually restart a server stuck like this.
  • Wait only 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

client.lua (slow)
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:

client.lua
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:

client.lua
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,
    })
end

lib.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:

Lua
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’s cache.ped keeps it for you.)
  • Use vector math: #(a - b) is faster and simpler than GetDistanceBetweenCoords.
  • 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.query with a callback, or .await inside 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:

Text
resmon 1

The 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:

Text
profiler record 500
profiler view
  • profiler record 500 records 500 frames (the docs recommend 500).
  • profiler status shows whether it’s still recording.
  • profiler view opens the result in Chrome’s performance view (on the server, it prints a link you open yourself).
  • profiler saveJSON myprofile.json saves 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.

Plays from youtube-nocookie.com after you click.

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_warning messages 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.