npcOptimize the performance of your FiveM and RedM scripts

-

Are you experiencing lag issues on your RedM or FiveM server?

In this article, we’ll provide you with practical tips to effectively optimize your scripts for FiveM and RedM, ensuring that your server runs smoothly. If you’re developing on RedM, we highly recommend the JO Library library. It’s an excellent tool to optimize your scripts easily with modules such as "Me" and "Timeout".

Avoiding Heavy Loops

  • Why it’s important: Poorly controlled loops can consume a lot of resources, especially when executed on every frame (tick), causing lag on your server.
  • Tips:
    • Use timers wisely: If a loop doesn’t need to run every frame, use timers (Wait) to slow it down. For example, instead of checking the player’s position every frame, you can check it every half second. This will be more than enough for 99% of scripts.
    • Limit infinite loops: The fewer infinite loops (while true do) you have, the better optimized your script will be.

Example:

Bad loop:
→ runs at 19.6ms (over 1000 frames)

--Loop running every frame
local shopCoordinate = vec3(3123.0,1243.0,51.0)
CreateThread(function()
  --Loop every frame
  while true do
    local ped = PlayerPedId() --retrieve player entity
    local coordinates = GetEntityCoords(ped) --retrieve coordinates
    --Check distance between player and shop
    if (#(coordinates-shopCoordinate) < 2.0) then
      print('You are in the zone')
    end
    Wait(0)
  end
end)

Optimized loop:
→ runs at 0.0ms (over 1000 frames)

--Optimized loop running every 500ms
local shopCoordinate = vec3(3123.0,1243.0,51.0) --shop coordinates
CreateThread(function()
  while true do --1-second loop
    local ped = PlayerPedId() --retrieve player entity
    local coordinates = GetEntityCoords(ped) --retrieve coordinates
    if (#(coordinates-shopCoordinate) < 2.0) then
      print('You are in the zone')
    end
    Wait(500) --pause for 1 second before next check
  end
end)

If you need a fast loop when the player is in the zone, you can use a double while loop. This way, the loop will run slowly when the player is far away, but faster when they are close.

--Optimized loop with variable speed
local shopCoordinate = vec3(3123.0,1243.0,51.0) --shop coordinates
CreateThread(function()
  while true do --1-second loop
    local ped = PlayerPedId()
    local coordinates = GetEntityCoords(ped)
    --New loop runs every frame as long as the player is close
    while (#(coordinates-shopCoordinate) < 2.0) do
      --Don't forget to update the player's coordinates
      coordinates = GetEntityCoords(ped) 
      print('You are in the zone')
      Wait(0)
    end
    --Pause for 1 second before the next check
    Wait(500)
  end
end)

Optimizing Entity and Object Management

  • Why it’s important: Entities (NPCs, objects, vehicles) consume server resources. Poor management can cause memory leaks and slowdowns.
  • Tips:
    • Remove unnecessary entities: If an NPC or vehicle is no longer needed, make sure to delete them properly to free up memory.
    • Use local entities when possible: When interaction with an entity doesn’t require synchronization between players, use local entities. This way, you can spawn them when the player gets close and delete them when they move away.

Example:
Creating an NPC for a shop:

--Optimized loop for entity creation
local pedModel = joaat('a_f_m_bevhills_02')
local pedCoordinate = vec4(3123.0,1243.0,51.0,143.0)
local shopCoordinate = vec3(3123.0,1243.0,51.0) --shop coordinates

CreateThread(function()
  local shopPed = 0
  --1-second loop
  while true do
    local ped = PlayerPedId()
    local coordinates = GetEntityCoords(ped)
    --2nd loop to manage shop NPC spawn if the player is within 10m
    while (#(coordinates - shopCoordinate) < 10.0) do
      if shopPed == 0 then --create the NPC
        shopPed = CreatePed(0, pedModel, pedCoordinate.xyz, pedCoordinate.w, false, false)
      end
      --3rd loop to manage shop interaction
      while (#(coordinates - shopCoordinate) < 2.0) do
        print('In the zone')
        coordinates = GetEntityCoords(ped)
        Wait(0)
      end
      coordinates = GetEntityCoords(ped)
      Wait(500)
    end
    --If the player is no longer close, delete the NPC
    if shopPed ~= 0 then
      DeleteEntity(shopPed)
      shopPed = 0
    end
    --Pause for 1 second before the next check
    Wait(500)
  end
end)

This may complicate the readability of the script, so it’s recommended to split the loop into two.

--Optimized double loop for entity creation
local pedModel = joaat('a_f_m_bevhills_02')
local pedCoordinate = vec4(3123.0,1243.0,51.0,143.0)
local shopCoordinate = vec3(3123.0,1243.0,51.0) --shop coordinates
local playerCoordinate = vec3(0,0,0) --store player position globally

--First loop to update player’s position
CreateThread(function()
  while true do
    local ped = PlayerPedId()
    playerCoordinate = GetEntityCoords(ped)
    Wait(500)
  end
end)

--Second loop to manage the shop
CreateThread(function()
  local shopPed = 0
  local distanceShop = 0
  local nextTestInMS = 0
  --1-second loop
  while true do
    --Store the distance between the player and the shop
    distanceShop = #(coordinates - shopCoordinate)
    if (distanceShop < 10.0) then
      --Create the NPC if it doesn’t exist
      if shopPed == 0 then
        shopPed = CreatePed(0, pedModel, pedCoordinate.xyz, pedCoordinate.w, false, false)
      end
      --Manage shop interaction
      if (distanceShop < 2.0) then
        nextTestInMS = 0
        print('In the zone')
      else
        --Pause for 1 second if the player is not close
        nextTestInMS = 500
      end
    else
      if shopPed ~= 0 then
        DeleteEntity(shopPed)
        shopPed = 0
      end
      --Pause for 1 second if the player is far away
      nextTestInMS = 500
    end
    Wait(nextTestInMS)
  end
end)

Even with the double infinite loop, the profiler shows 0.00ms usage thanks to Wait(500), which drastically reduces script consumption.

Optimizing Database Access

  • Why it’s important: Frequent database access can create bottlenecks and slow down your server.
  • Tips:
    • Cache data: Instead of making SQL queries every time a piece of data is needed, cache it in a server-side variable. This allows you to access the value without having to re-query the database.
    • Use asynchronous queries: Synchronous SQL queries can block the script execution. Make sure to use asynchronous queries to avoid blocking the server while waiting for a database response.
    • Use the database for writing: As your server grows and more players connect, the number of SQL queries can increase quickly. We recommend adopting a new approach: Load all necessary data when the script starts, cache it, and only update the database asynchronously when needed.
    • Use advanced SQL queries: If you need to read data from multiple tables, it’s recommended to merge your calls into a single SQL query using joins such as LEFT JOIN.

Example:

-- Bad example: Synchronous query
local result = MySQL.query.await('SELECT * FROM players WHERE id = @id', {id = playerId})
local skin = MySQL.query.await('SELECT * FROM skins WHERE identifier = @identifier', {identifier = player.identifier})
print(json.encode(skin))

-- Good example: Asynchronous query
MySQL.query('SELECT * FROM players LEFT JOIN skin ON players.identifier = skin.identifier WHERE player.id = @id', {id = playerId}, function(result)
  print(json.encode(skin))
end)

Be careful: Asynchronous queries don’t block the script, so if you need to return values, you may need to use Await. In such cases, synchronous queries may be more appropriate.

Optimizing Native Function Usage

  • Why it’s important: Natives are functions provided by FiveM and RedM to interact with the game, but some can be very resource-intensive if used inefficiently. Poor use of natives can lead to CPU overload and cause significant server or client lag.
  • Tips:
    • Avoid repeated calls to expensive natives: Some natives, such as GetVehiclePedIsIn or IsEntityOnScreen, are often called every frame. To optimize this, cache these values when possible, and only update them when necessary rather than calling them repeatedly in a fast loop.
    • Check before calling: Before using a native, make sure the entities or objects you’re working with still exist. For example, before calling DeleteEntity, verify that the entity exists with DoesEntityExist to avoid unnecessary calls or errors.

Example:

Unoptimized loop with multiple calls to the same native:

local shop = vec3(1234.0,224.0,32.0)
CreateThread(function()
  while true do
    local ped = PlayerPedId()
    local coords = GetEntityCoords(PlayerPedId())
    --1st use of Vdist
    if Vdist(coords,shop) < 10 then
      SpawnPed()
    end
    --2nd use of Vdist
    if Vdist(coords,shop) < 2.0 then
      OpenShop()
    end
    Wait(0) -- The loop runs every tick
  end
end)

Optimized loop with caching:

--Optimized loop with caching of Vdist result
CreateThread(function()
  while true do
    local ped = PlayerPedId()
    local coords = GetEntityCoords(PlayerPedId())
    local distance = Vdist(coords,shop) --cache the result of Vdist
    --Use cache instead of the native call
    if distance < 10 then
      SpawnPed()
    end
    if distance < 2.0 then
      OpenShop()
    end
    Wait(500) -- The loop runs every 500ms
  end
end)

By caching the values of expensive natives like GetEntityCoords or spacing out their calls, you can significantly reduce CPU usage and improve overall server performance.

Identifying Lag-Causing Scripts

  • Why it’s important: Poorly optimized scripts can overload the CPU, leading to performance drops and significant lag for players. Identifying these scripts allows you to target optimizations and ensure a smoother gameplay experience.
  • Tips:
    • Monitor with resmon: On the client side, you can activate the Resource Monitor (resmon) by typing resmon 1 in the F8 console. This tool shows a real-time graph of CPU usage by each resource on the client, helping you identify which resources are consuming the most and need optimization.
    • Use the profiler: Once you’ve identified the heavy resource usage via resmon, you can use the profiler for more detailed results. Type profiler start <number of frames> in the server console to activate the profiler and view detailed CPU usage data with profiler view. Running it for 500 frames is a good starting point to identify heavy scripts. Focus on scripts that consistently consume over 5ms of CPU time.
    • Analyze the results: If a script regularly exceeds 5ms of CPU usage, it typically indicates a performance issue. Review these scripts first and optimize the most resource-intensive parts.
Optimize your resources with the resmon of FiveM and RedM
FiveM & RedM Resmon
Optimize your resources with the profiler of FiveM and RedM
FiveM & RedM Profiler

Conclusion

Optimizing RedM and FiveM scripts is an ongoing process. By applying these best practices, you can not only reduce the load on your server but also offer your players a much smoother gaming experience. Don’t forget to regularly analyze the performance of your scripts, adjust your code accordingly, and always stay up-to-date with the latest optimization techniques. Resmon is a great tool to get an overview, and you can refine your results using the profiler to identify the most resource-intensive functions.