El Libro·Capítulo 5/12·11 min

Rendimiento: que tu servidor vuele

Cada recurso consume CPU en cada frame. Aprende a medir con resmon y a escribir bucles que no fundan los FPS de tus jugadores.

Un servidor de FiveM no se cae por culpa de un solo recurso pesado: se cae por la suma de muchos recursos mal escritos. Cada script que añades pide su trocito de CPU en CADA frame del juego, varias veces por segundo. Cuando esa suma se dispara, el síntoma no es «el recurso X va lento»: es que TODO el servidor tartamudea, los coches se teletransportan y los jugadores notan tirones (stuttering). Optimizar no es un lujo de servidores grandes; es la diferencia entre 64 jugadores cómodos y 20 jugadores quejándose.

La unidad que importa: ms por frame

El cliente de FiveM intenta dibujar muchos frames por segundo. En cada frame ejecuta el código de todos tus recursos. Si entre todos tardan demasiado, no le da tiempo a terminar el frame y los FPS caen. Por eso la métrica clave no son los megas de RAM ni el tamaño del recurso: son los milisegundos que consume por frame. Un recurso que gasta 0.10 ms es excelente; uno que gasta 5.00 ms se está comiendo tu servidor él solo.

Tu mejor amigo: el comando resmon

FiveM trae un monitor de recursos integrado. Abre la consola del cliente (F8) o escribe el comando en el chat y verás una tabla en vivo con cada recurso y lo que consume. Es la herramienta nº 1 para encontrar al culpable.

text
resmon      -- abre el monitor de recursos (resource monitor)
resmon 1    -- modo detallado: muestra también el uso del servidor

-- Columnas que te interesan:
--   CPU msec  -> milisegundos por frame (¡la importante!)
--   Time (%)  -> porcentaje del frame que consume
--   Memory    -> RAM del recurso

Comandos del monitor (consola F8)

FiveM colorea la columna de tiempo para que no tengas que adivinar: verde significa que va sobrado, amarillo que conviene vigilarlo y rojo que ese recurso está costando caro y hay que intervenir.

  • Verde (< 0.50 ms): perfecto, ni te preocupes.
  • Amarillo (0.50–1.00 ms aprox.): aceptable, pero revísalo si se acumulan varios.
  • Rojo (> 1.00 ms): problema. Ese recurso necesita optimización.
  • Idle alto: si un recurso gasta CPU cuando NADIE lo está usando (un menú cerrado, una tienda vacía), es la señal más clara de un bucle mal hecho.

El dato más revelador del resmon es el consumo en reposo. Un recurso bien escrito gasta casi 0 ms cuando no pasa nada. Si un script de tiendas marca 0.80 ms sin que nadie esté en una tienda, está corriendo en bucle aunque no haga falta.

El gran pecado: bucles sin Wait

Casi todos los desastres de rendimiento en FiveM tienen el mismo origen: un bucle infinito que se ejecuta demasiadas veces por segundo. En Lua se escribe con CreateThread y un while true do. El problema es el Wait que pones dentro. Wait(0) significa «vuelve a ejecutarme en el próximo frame», es decir, decenas de veces por segundo. Si dentro de ese bucle haces trabajo pesado, lo estás repitiendo sin parar aunque no haga ninguna falta.

lua
-- ❌ ANTES: dibuja un marcador y comprueba la tecla 60+ veces por segundo,
-- estés donde estés, aunque la tienda esté a 2 km. Esto se pone ROJO.
local tienda = vector3(25.7, -1345.0, 29.5)

CreateThread(function()
  while true do
    Wait(0) -- cada frame, SIEMPRE
    local ped = PlayerPedId()
    local pos = GetEntityCoords(ped)
    DrawMarker(1, tienda.x, tienda.y, tienda.z - 1.0, 0,0,0, 0,0,0, 1.0,1.0,1.0, 0,150,255,100, false,false,2,nil,nil,false)
    if #(pos - tienda) < 1.5 then
      if IsControlJustPressed(0, 38) then -- tecla E
        abrirTienda()
      end
    end
  end
end)

Bucle que funde los FPS

La solución: Wait dinámico

La técnica que separa a un script amateur de uno profesional es ajustar el Wait según lo que esté pasando. La idea es simple: si el jugador está lejos del punto de interés, no necesitas dibujar nada ni mirar el teclado, así que duerme el bucle un buen rato (por ejemplo 1000 ms). Solo cuando el jugador está cerca bajas a Wait(0) para que el marcador y la tecla respondan al instante. El resultado: el recurso pasa casi todo el tiempo en reposo y consume prácticamente 0 ms hasta que de verdad hace falta.

lua
-- ✅ DESPUÉS: el mismo comportamiento, pero en reposo gasta casi 0 ms.
local tienda = vector3(25.7, -1345.0, 29.5)

CreateThread(function()
  while true do
    local sleep = 1000 -- por defecto, duerme 1 segundo
    local pos = GetEntityCoords(PlayerPedId())
    local dist = #(pos - tienda)

    if dist < 20.0 then
      sleep = 0 -- cerca: respondemos cada frame
      DrawMarker(1, tienda.x, tienda.y, tienda.z - 1.0, 0,0,0, 0,0,0, 1.0,1.0,1.0, 0,150,255,100, false,false,2,nil,nil,false)
      if dist < 1.5 and IsControlJustPressed(0, 38) then
        abrirTienda()
      end
    end

    Wait(sleep)
  end
end)

Wait dinámico por distancia

Fíjate en el detalle: cuando el jugador está a 2 km, el bucle solo calcula UNA distancia cada segundo y se vuelve a dormir. Eso es trabajo despreciable. Solo dentro del radio de 20 metros baja a 0 ms para que el marcador se vea fluido. Un mismo script puede pasar de 1.20 ms (rojo) a 0.01 ms (verde) con este único cambio.

Cachea las natives caras

Las funciones nativas como PlayerPedId() o PlayerId() no son gratis. Llamarlas una vez no se nota; llamarlas cinco veces por iteración dentro de un bucle a Wait(0), sí. Sácalas del bucle apretado y guarda el resultado en una variable. El ped del jugador no cambia entre líneas: no hay razón para pedirlo una y otra vez.

lua
-- ❌ pide el ped y las coords varias veces por frame
CreateThread(function()
  while true do
    Wait(0)
    if GetEntityHealth(PlayerPedId()) < 50 then avisar() end
    if IsPedSwimming(PlayerPedId()) then nadar() end
    local c = GetEntityCoords(PlayerPedId())
  end
end)

-- ✅ cachea el ped una vez por iteración
CreateThread(function()
  while true do
    Wait(500)
    local ped = PlayerPedId() -- una sola llamada
    if GetEntityHealth(ped) < 50 then avisar() end
    if IsPedSwimming(ped) then nadar() end
    local c = GetEntityCoords(ped)
  end
end)

Cachear natives fuera del trabajo repetido

Mide por distancia con #(a - b)

La regla de oro del rendimiento en cliente es: no hagas trabajo si el jugador no está cerca. Para eso necesitas medir distancias, y FiveM te da una forma rapidísima con los tipos vector. El operador #(vec1 - vec2) calcula la distancia entre dos puntos directamente, sin la fórmula manual con raíces cuadradas. Úsalo como portero de tus bucles: primero la distancia, y solo si pasa el filtro, el resto del trabajo.

lua
local pos = GetEntityCoords(PlayerPedId())
local punto = vector3(-1037.0, -2738.0, 20.0)

local distancia = #(pos - punto) -- distancia en metros, ya está

if distancia < 50.0 then
  -- solo aquí dibujamos, comprobamos teclas, etc.
end

Distancia con vectores

Eventos antes que polling constante

Un bucle que pregunta «¿ya? ¿ya? ¿ya?» todo el rato (polling) casi siempre se puede sustituir por algo que te avise solo cuando ocurre. En lugar de revisar a mano si el jugador entró en una zona, usa herramientas que ya hacen ese trabajo de forma optimizada: ox_target o PolyZone para zonas e interacciones, y statebags para reaccionar a cambios de estado de una entidad sin vigilarla en bucle. Reutilizar estas librerías no es pereza: es delegar el trabajo pesado en código que ya está optimizado y probado por miles de servidores.

  • Interacciones de proximidad (entrar a una tienda, abrir un maletero): usa ox_target en vez de un bucle de DrawMarker propio.
  • Zonas geográficas (áreas seguras, perímetros): usa PolyZone, que dispara onEnter/onExit por ti.
  • Reaccionar a un cambio de dato (un jugador esposado, un vehículo cerrado): usa statebags y AddStateBagChangeHandler.

Limpia las entidades que creas

Si tu script crea objetos, props, peds o vehículos, eres responsable de borrarlos. Cada entidad viva consume recursos y, peor aún, las entidades huérfanas se acumulan: un script de decoración que crea props sin guardarse el handle deja basura por el mapa que nadie puede limpiar. Guarda siempre la referencia y elimínala cuando ya no la necesites, sobre todo al parar el recurso.

lua
local props = {}

local function crearProp(modelo, coords)
  local obj = CreateObject(modelo, coords.x, coords.y, coords.z, true, true, false)
  props[#props + 1] = obj -- guardamos el handle para poder borrarlo
  return obj
end

-- Limpieza obligatoria al detener el recurso:
AddEventHandler('onResourceStop', function(res)
  if res ~= GetCurrentResourceName() then return end
  for _, obj in ipairs(props) do
    if DoesEntityExist(obj) then DeleteEntity(obj) end
  end
end)

Crear y limpiar entidades

El lado servidor también cuenta

El servidor tiene un solo hilo para la lógica del juego. Si lo bloqueas, lo bloqueas para TODOS los jugadores a la vez. El error clásico es hacer consultas a la base de datos dentro de un bucle, o muchas consultas seguidas donde bastaría una. Con oxmysql usa la versión asíncrona (.await) para no congelar el hilo mientras esperas a la base de datos, y nunca consultes dentro de un for: agrupa los datos en una sola query.

lua
-- ❌ una consulta por jugador dentro de un bucle: N viajes a la BD
for _, id in ipairs(idsJugadores) do
  local row = MySQL.single.await('SELECT dinero FROM users WHERE id = ?', { id })
  -- ...
end

-- ✅ una sola consulta con IN para todos los jugadores
local rows = MySQL.query.await(
  'SELECT id, dinero FROM users WHERE id IN (?)',
  { idsJugadores }
)
-- y, en la tabla, un índice en la columna que filtras (id ya es PRIMARY KEY)

Una query en vez de N (servidor)

Dos detalles más en el servidor: asegúrate de tener índices en las columnas por las que filtras (un WHERE sobre una columna sin índice obliga a la base de datos a leer la tabla entera), y no uses .await dentro de un bucle apretado: aunque no bloquea el hilo, encadenar cientos de esperas seguidas igual ralentiza la operación. Agrupa siempre que puedas.

Checklist de optimización

  • ¿Todos tus bucles usan Wait dinámico (suben el Wait cuando no hace falta refrescar)?
  • ¿Hay algún Wait(0) permanente que se pueda condicionar a la distancia?
  • ¿Cacheas PlayerPedId() y las coords fuera del trabajo repetido?
  • ¿Filtras por distancia con #(a - b) antes de dibujar o interactuar?
  • ¿Has sustituido polling por ox_target, PolyZone o statebags donde encaje?
  • ¿Borras las entidades que creas, también en onResourceStop?
  • ¿Las consultas SQL están fuera de los bucles, son .await y las columnas filtradas tienen índice?
  • ¿Comprobaste el resmon en reposo y ningún recurso se queda en amarillo/rojo sin actividad?

Regla de oro: mide con resmon ANTES y DESPUÉS de cada cambio. No optimices a ciegas ni por intuición: abre el monitor, anota los ms del recurso, aplica una mejora y comprueba que el número baja de verdad. Si no bajó, el cuello de botella estaba en otro sitio.

¿Una duda sobre esto? El chat de la IA lo sabe todo y te responde con código.

Pregunta a la IA
Optimización en FiveM: resmon, Wait y buenas prácticas