Retour au cours

games / fivem

Optimisation réseau : bande passante et netEvent throttling

Leçon 231 exercice

Explication

Ce que vous allez apprendre

  • Identifier pourquoi un event déclenché à chaque frame sature la bande passante à l'échelle
  • Throttler l'envoi d'un event par intervalle de temps ET par seuil de changement significatif
  • Regrouper plusieurs petites mises à jour en un seul event (batching)
  • Envoyer uniquement un delta plutôt que l'état complet d'une donnée
  • Mesurer la taille réelle d'un payload avant d'optimiser à l'aveugle

Dans quel contexte ?

Un serveur à soixante-quatre joueurs commence à souffrir de latence perceptible pour tout le monde, alors que le CPU du serveur lui-même semble avoir de la marge. L'enquête révèle qu'une ressource envoie la position de chaque joueur à chaque frame (jusqu'à 3840 events par seconde cumulés) pour un simple affichage sur la carte — un cas d'école que cette leçon apprend à corriger avec un throttling adapté.

TechniqueEffet
Intervalle minimum (Wait(500) au lieu de Wait(0))Divise déjà la fréquence par 30
Seuil de changement significatifN'envoie que si la donnée a réellement bougé
BatchingUn seul event pour plusieurs changements
Delta au lieu de l'état completRéduit la taille de chaque payload

Piège fréquent

Déclencher un TriggerServerEvent à chaque frame (Citizen.Wait(0)) pour une donnée qui change rarement de façon significative, comme une position, est l'erreur réseau la plus coûteuse et pourtant la plus fréquente sur les serveurs FiveM.

Un coût invisible qui devient énorme à l'échelle

Chaque event réseau (TriggerServerEvent, TriggerClientEvent) n'est pas gratuit : il consomme de la bande passante réelle, sur le serveur comme sur chaque client. Un seul event envoyé une fois par seconde par un seul joueur semble négligeable ; multiplié par soixante fois par seconde et par soixante-quatre joueurs simultanés, comme montré dans l'anti-pattern de cette leçon, le total devient rapidement suffisant pour saturer la capacité réseau du serveur entier.

Le réflexe : ralentir la fréquence sans perdre l'essentiel

La première optimisation, la plus simple, consiste à réduire la fréquence de vérification (vérifier deux fois par seconde plutôt qu'à chaque frame). La seconde, complémentaire, consiste à n'envoyer une mise à jour QUE si un changement réellement significatif a eu lieu depuis le dernier envoi — inutile de renvoyer une position qui n'a pratiquement pas bougé.

Le batching : grouper plutôt que multiplier

Envoyer un event séparé pour chaque petit changement individuel (chaque objet modifié dans un inventaire, par exemple) multiplie inutilement le nombre de messages réseau. Le "batching" consiste à accumuler plusieurs changements dans une file d'attente, puis à les envoyer groupés en un seul event à intervalle régulier — un seul message réseau au lieu de potentiellement des dizaines.

Envoyer la différence, pas l'état complet

Retransmettre systématiquement un objet de données entier, même quand une seule de ses valeurs a changé, gaspille de la bande passante sur des informations déjà connues du destinataire. Calculer et n'envoyer que le "delta" (ce qui a réellement changé depuis le dernier envoi) réduit la taille de chaque message au strict nécessaire.

Mesurer avant d'optimiser à l'aveugle

Optimiser sans données réelles revient à deviner. Logger la taille effective des payloads envoyés permet d'identifier objectivement quels events sont les plus coûteux en pratique, pour concentrer l'effort d'optimisation là où il aura le plus d'impact réel, plutôt que de partir sur des suppositions.

Commandes & code

Optimisation réseau : bande passante et netEvent throttling

Chaque TriggerServerEvent/TriggerClientEvent a un coût réseau réel : à 64 joueurs, un event mal throttlé peut saturer la bande passante du serveur.

lua
-- Anti-pattern classique : déclencher un event serveur à CHAQUE frame (jusqu'à 60x/seconde) pour une simple position
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)   -- 0 = chaque frame, ~60 fois/seconde
        local coords = GetEntityCoords(PlayerPedId())
        TriggerServerEvent('technologik:updatePosition', coords)   -- 60 events/s/joueur x 64 joueurs = 3840 events/s !
    end
end)
lua
-- Throttling correct : n'envoyer qu'au changement significatif ET avec un intervalle minimum
local lastSentCoords = vector3(0, 0, 0)
local lastSentTime = 0

Citizen.CreateThread(function()
    while true do
        Citizen.Wait(500)   -- vérifie 2 fois/seconde au lieu de 60 : divise déjà la charge par 30
        local coords = GetEntityCoords(PlayerPedId())
        local distanceMoved = #(coords - lastSentCoords)

        -- N'envoie que si le joueur a réellement bougé de façon significative
        if distanceMoved > 1.0 then
            TriggerServerEvent('technologik:updatePosition', coords)
            lastSentCoords = coords
            lastSentTime = GetGameTimer()
        end
    end
end)
lua
-- Batching : regrouper plusieurs petites mises à jour en un seul event au lieu d'en envoyer une par élément
local PendingUpdates = {}

local function queueInventoryUpdate(slot, item, count)
    PendingUpdates[slot] = { item = item, count = count }
end

-- Un seul thread flush la file toutes les 250ms au lieu d'un event par appel à queueInventoryUpdate
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(250)
        if next(PendingUpdates) ~= nil then
            TriggerServerEvent('technologik:batchInventoryUpdate', PendingUpdates)   -- UN event, N changements
            PendingUpdates = {}
        end
    end
end)
lua
-- Mesurer le coût réseau réel d'un event pour prioriser les optimisations (profiling niveau expert)
-- via la commande native "net" en jeu (F8) : "net_perframeupdate" et le profiler réseau intégré au serveur

-- Côté script : logger la taille sérialisée d'un payload avant envoi pour identifier les events les plus coûteux
local function loggedTriggerServerEvent(eventName, payload)
    local serialized = json.encode(payload)
    if #serialized > 2000 then   -- seuil arbitraire : au-delà, un event mérite d'être découpé ou compressé
        print(('[NET WARNING] Event "%s" envoie %d octets, envisager du découpage'):format(eventName, #serialized))
    end
    TriggerServerEvent(eventName, payload)
end

-- Réduire un payload répétitif : envoyer des deltas plutôt que l'état complet
local function computeDelta(previous, current)
    local delta = {}
    for k, v in pairs(current) do
        if previous[k] ~= v then
            delta[k] = v   -- ne transmet QUE ce qui a changé, pas l'objet entier
        end
    end
    return delta
end

Résumé

  • Un TriggerServerEvent en boucle à chaque frame (Citizen.Wait(0)) est l'erreur réseau la plus coûteuse et la plus fréquente.
  • Throttler par intervalle de temps ET par seuil de changement significatif réduit drastiquement le nombre d'events envoyés.
  • Le batching regroupe plusieurs petites mises à jour en un seul event au lieu d'un event par élément modifié.
  • Envoyer des deltas (uniquement ce qui a changé) plutôt que l'état complet réduit la taille de chaque payload réseau.

Exercices pratiques

1 disponible
1

Mission : trouver l'event qui sature le réseau du serveur

Objectif : Diagnostiquer un event réseau spammé sans réel besoin, le corriger par throttling, et raisonner sur les limites du batching.

Contexte

Le serveur technologik_rp, à 64 joueurs, souffre de latence perceptible pour tout le monde alors que le CPU serveur a de la marge. L'enquête révèle qu'une ressource de carte GPS envoie TriggerServerEvent('technologik:updatePosition', coords) à chaque frame pour chaque joueur, alors que la carte en jeu ne se rafraîchit visuellement qu'une fois par seconde.

Résoudre l’exercice →