Sale on selected scripts — limited time only.

VIP membership — full catalog access, one monthly price.

Browse the shop for current deals.

BlogArtikel
FIG. 01/Performance · 30. Januar 2025

FiveM Server Performance optimieren: Der vollständige Guide

FusionDev Team16 Minuten Lesezeit30.01.2025Performance

Performance ist keine Option — sie ist Grundvoraussetzung

Nichts verliert Spieler schneller als ein lagender Server. Nicht fehlende Features, nicht unfertige Maps, nicht inkomplete Jobs — Lag. Spieler tolerieren unfertige Content, aber sie verlassen einen Server der sich schlecht anfühlt innerhalb von Minuten. Und sie kommen oft nicht zurück.

Das Tückische: Performance-Probleme auf FiveM-Servern haben viele Ursachen und zeigen sich nicht immer eindeutig. Ein Script das bei 20 Spielern problemlos läuft, kann bei 60 Spielern den Server zum Einbruch bringen. Dieser Guide gibt dir die Werkzeuge und das Wissen, um Performance-Probleme systematisch zu identifizieren und zu beheben.

Das Grundverständnis: Was FiveM-Performance beeinflusst

FiveM-Server-Performance wird von mehreren Faktoren gleichzeitig beeinflusst:

  • Server-CPU: Lua-Scripts laufen in Threads auf dem Server. Zu viele aktive Threads oder ineffiziente Loops überlasten die CPU.
  • Datenbankperformance: Jede SQL-Abfrage kostet Zeit. Schlecht strukturierte Queries oder fehlende Indexe können einen Server lahmlegen.
  • Netzwerk-Overhead: Zu viele Events, zu viele State-Updates, zu viele Streaming-Ressourcen überlasten die Netzwerk-Verbindung zwischen Server und Clients.
  • Client-seitige Last: Scripts die auf jedem Client laufen und dort die CPU belasten, erzeugen schlechte Frame-Rates beim Spieler — auch bei gutem Server.
  • Memory-Leaks: Scripts die Tabellen oder Objekte aufbauen ohne sie freizugeben, fressen über Zeit immer mehr RAM.

Schritt 1: Resmon richtig lesen

Der FiveM Resource Monitor (resmon in der F8-Konsole) ist dein wichtigstes Diagnosewerkzeug. Viele Betreiber öffnen ihn, sehen eine Tabelle mit Zahlen und wissen nicht was sie bedeuten.

Die wichtigsten Spalten:

  • MS (Avg): Durchschnittliche CPU-Zeit pro Tick. Das ist die wichtigste Metrik.
  • MS (Max): Der Spitzenwert. Zeigt kurze Performance-Einbrüche an, auch wenn der Durchschnitt gut aussieht.
  • MS (Min): Minimaler Wert — für die Diagnose weniger relevant.

Zielwerte:

  • Unter 0.05ms im Idle: Exzellent. Das Script ist gut optimiert.
  • 0.05ms – 0.5ms: Akzeptabel, je nach Komplexität.
  • 0.5ms – 2ms: Problematisch. Untersuche das Script.
  • Über 2ms: Kritisch. Dieses Script muss sofort adressiert werden.

Wichtig: Diese Werte addieren sich. Wenn du 50 Scripts hast die je 0.1ms verbrauchen, sind das insgesamt 5ms — und dein Server-Tick kann 50ms dauern, was direkt zu spürbarem Lag führt.

Geh methodisch vor: Sortiere nach "MS (Avg)", dokumentiere die Top-10-Verbraucher, und arbeite von oben nach unten. Oft verursachen 2-3 schlecht optimierte Scripts 70-80% der gesamten Last.

Schritt 2: Thread-Optimierung — die häufigste Fehlerquelle

Schlecht konfigurierte Threads sind die häufigste Ursache für hohen Resmon-Verbrauch. Das Anti-Pattern schlechthin:

-- SCHLECHT: Läuft 60x pro Sekunde, macht aber eigentlich nichts zeitkritisches
Citizen.CreateThread(function()
  while true do
    Citizen.Wait(0)
    checkIfPlayerNearShop()     -- Reicht alle 500ms
    updateHUD()                  -- Reicht alle 100ms
    syncDataToOtherPlayers()     -- Reicht alle 1000ms
  end
end)

Die Lösung ist adaptive Wait-Zeit: Bestimme wie oft eine Funktion wirklich laufen muss und setze den Wait-Wert entsprechend.

-- GUT: Verschiedene Threads mit angepassten Intervallen
Citizen.CreateThread(function()
  while true do
    Citizen.Wait(500)  -- Nur 2x pro Sekunde
    checkIfPlayerNearShop()
  end
end)

Citizen.CreateThread(function()
  while true do
    Citizen.Wait(100)  -- 10x pro Sekunde reicht für HUD
    updateHUD()
  end
end)

Citizen.CreateThread(function()
  while true do
    Citizen.Wait(1000)  -- 1x pro Sekunde für Sync
    syncDataToOtherPlayers()
  end
end)

Ein Thread mit Citizen.Wait(0) verbraucht etwa 60x mehr CPU als einer mit Citizen.Wait(1000). Nutze Wait(0) nur wenn es wirklich frame-genaue Verarbeitung erfordert — Fahrzeug-Physik, präzise Input-Erkennung, Animations-Blending.

Schritt 3: Distance Checks — nur berechnen was nötig ist

Viele Scripts führen Berechnungen für alle Spieler oder alle Entities global durch, unabhängig davon ob der Spieler sich überhaupt in der Nähe befindet. Das ist extrem ineffizient auf Servern mit vielen Spielern oder vielen Interaktionspunkten.

-- Das Muster: dynamischer Wait-Wert basierend auf Distanz
Citizen.CreateThread(function()
  while true do
    local playerCoords = GetEntityCoords(PlayerPedId())
    local closestDist = math.huge
    local inRange = false

    for _, location in ipairs(Config.Locations) do
      local dist = #(playerCoords - location.coords)
      if dist < closestDist then closestDist = dist end
      if dist < Config.InteractionRange then
        inRange = true
        DrawInteraction(location)
      end
    end

    -- Weit weg: seltener prüfen, nah dran: öfter
    if closestDist > 100.0 then
      Citizen.Wait(3000)
    elseif closestDist > 50.0 then
      Citizen.Wait(1000)
    elseif closestDist > 20.0 then
      Citizen.Wait(500)
    else
      Citizen.Wait(0)  -- Nur wenn wirklich nah dran
    end
  end
end)

Dieses Muster reduziert den CPU-Verbrauch von Standort-Scripts um bis zu 90%, weil die schwere Arbeit nur dann passiert wenn der Spieler auch tatsächlich in der Nähe ist.

Schritt 4: Event-Throttling und Security

Events sind der Kommunikationskanal zwischen Client und Server. Unkontrolliert gefeuerte Events überlasten das Netzwerk und den Server. Zwei Probleme treten häufig auf:

Problem 1: Events in Loops ohne Throttling

-- SCHLECHT: Feuert Events jeden Frame
Citizen.CreateThread(function()
  while true do
    Citizen.Wait(0)
    TriggerServerEvent('myScript:updatePosition', GetEntityCoords(PlayerPedId()))
  end
end)

-- GUT: Mit Throttling
local lastUpdate = 0
Citizen.CreateThread(function()
  while true do
    Citizen.Wait(0)
    local now = GetGameTimer()
    if now - lastUpdate > 200 then  -- Maximal 5x pro Sekunde
      lastUpdate = now
      TriggerServerEvent('myScript:updatePosition', GetEntityCoords(PlayerPedId()))
    end
  end
end)

Problem 2: Fehlende Rate-Limiting auf Server-Events

-- Server: Cooldown pro Spieler implementieren
local cooldowns = {}

RegisterNetEvent('myScript:doAction')
AddEventHandler('myScript:doAction', function()
  local src = source
  local now = os.time()

  if cooldowns[src] and (now - cooldowns[src]) < 5 then
    -- Spieler ignorieren oder warnen
    return
  end

  cooldowns[src] = now
  -- Eigentliche Logik
end)

Das Server-seitige Rate-Limiting ist auch ein wichtiges Sicherheits-Feature. Ohne es können Exploits Events hunderte Male pro Sekunde feuern und den Server überlasten oder ausnutzen.

Schritt 5: Datenbankoptimierung im Detail

Bei 50+ Spielern ist die Datenbank oft der größte Flaschenhals. Konkrete Maßnahmen:

Indexe prüfen und setzen

-- Prüfe ob deine häufigsten Queries einen Index nutzen
EXPLAIN SELECT * FROM players WHERE citizenid = 'XYZ123';

-- Wenn "type" = "ALL" in der Ausgabe: Full-Table-Scan, Index fehlt!
-- Index erstellen:
CREATE INDEX idx_citizenid ON players (citizenid);
CREATE INDEX idx_identifier ON users (identifier);

-- Für häufig gefilterte Felder:
CREATE INDEX idx_job ON users (job);
CREATE INDEX idx_group ON users (group);

Async Queries — niemals synchron blockieren

-- SCHLECHT: Synchrone Query blockiert den Server-Thread
local result = MySQL.Sync.fetchAll('SELECT * FROM players WHERE citizenid = ?', {citizenid})

-- GUT: Async mit Callback
MySQL.query('SELECT * FROM players WHERE citizenid = ?', {citizenid}, function(result)
  if result and result[1] then
    -- Verarbeitung hier
  end
end)

-- NOCH BESSER: Promise-basiert mit oxmysql
local result = MySQL.query.await('SELECT * FROM players WHERE citizenid = ?', {citizenid})
if result and result[1] then
  -- Verarbeitung
end

Queries bündeln

-- SCHLECHT: Viele einzelne Updates
for _, item in ipairs(playerItems) do
  MySQL.query('UPDATE items SET count = ? WHERE id = ?', {item.count, item.id})
end

-- GUT: Transaktion oder Batch-Insert
local queries = {}
for _, item in ipairs(playerItems) do
  table.insert(queries, {
    query = 'UPDATE items SET count = ? WHERE id = ?',
    values = {item.count, item.id}
  })
end
MySQL.transaction(queries)

Schritt 6: Streaming-Ressourcen optimieren

Streaming-Ressourcen (Custom-Vehicles, Kleidung, Maps) werden an jeden verbindenden Client übertragen. Überdimensionierte Assets verlängern Ladezeiten und erhöhen den Netzwerk-Overhead.

  • YTD-Texturen komprimieren: Nutze CodeWalker oder OpenIV um Texturen zu komprimieren. 4K-Texturen für Interior-Details die man kaum sieht sind Ressourcenverschwendung. 512x512 oder 1024x1024 reicht für die meisten Assets.
  • YFT-Modelle optimieren: Reduziere Polygon-Count für Fahrzeuge die hauptsächlich als Hintergrund-Deko dienen. Ein 400k-Poly-Modell für ein geparktes Auto ist übertrieben.
  • Audio komprimieren: WAV-Dateien durch OGG ersetzen. Gleiche Qualität, Bruchteil der Dateigröße.
  • Streaming-Limits prüfen: FiveM hat Limits für gleichzeitig streamende Ressourcen. Wenn du sie erreichst, crashen Clients beim Joinen.

Schritt 7: Memory-Leaks aufspüren

Memory-Leaks zeigen sich schleichend: Der Server läuft morgens problemlos, abends nach 12 Stunden Betrieb wird er langsamer. Der RAM-Verbrauch wächst kontinuierlich. Neustart hilft kurz, dann beginnt es wieder.

-- Typisches Memory-Leak: Tabelle wächst ohne zu schrumpfen
local entityCache = {}

AddEventHandler('entityCreated', function(entity)
  entityCache[entity] = { created = os.time(), data = GetEntityCoords(entity) }
  -- Problem: entityCache wächst für jede jemals erstellte Entity
  -- Entities die gelöscht werden, bleiben in der Tabelle
end)

-- Fix: Cleanup beim Löschen
AddEventHandler('entityRemoved', function(entity)
  entityCache[entity] = nil
end)

Überwache regelmäßig den RAM-Verbrauch deines Server-Prozesses. Ein Anstieg von mehr als 100-200MB über 24 Stunden deutet auf Leaks hin. Identifiziere das verantwortliche Script durch schrittweises Deaktivieren.

Schritt 8: Monitoring dauerhaft einrichten

Einmalige Optimierung reicht nicht. Jedes neue Script, jedes Update kann neue Performance-Probleme einführen. Richte dauerhaftes Monitoring ein:

  • Automatische Resmon-Logs: Scripts wie monitor von CFX können Resmon-Daten regelmäßig in eine Datenbank schreiben und Trends sichtbar machen.
  • Discord-Alerts: Richte Webhooks ein die warnen wenn ein Script über einen Schwellenwert steigt oder wenn der Server crasht.
  • Player-Count Korrelation: Beobachte wie sich Performance-Metriken mit der Spielerzahl entwickeln. So erkennst du welche Scripts nicht für hohe Spielerzahlen skalieren.
  • Regelmäßige Neustarts: Plane tägliche Neustarts in Zeiten geringer Spieleraktivität. Das umgeht Memory-Leaks die du noch nicht identifiziert hast und gibt dem Server einen sauberen Neustart.
-- server.cfg: Automatischer Neustart via txAdmin oder Cron
# Täglich um 05:00 Uhr
0 5 * * * /path/to/restart-script.sh

Die goldene Regel: Messen, nicht raten

Der größte Fehler bei Performance-Optimierung ist das Raten. "Ich glaube das Script ist schuld" führt zu falschen Prioritäten und verschwendeter Zeit. Nutze immer Resmon, aktiviere Query-Logging in deiner Datenbank (slow_query_log in MySQL), und lass Zahlen entscheiden — nicht Bauchgefühl.

Ein Server der für 100 Spieler konzipiert ist und bei 80 bereits laggt, hat ein grundlegendes Problem. Ein Server der bei 100 Spielern problemlos läuft, hat eine solide Grundlage für Wachstum. Investiere in die Grundlage.

FIG. 03/Shop

Bereit für den nächsten Schritt?

Entdecke unsere Script-Kollektion — optimiert, getestet und mit direktem Support.