GuidesLua
Performance Optimization
How to keep your BeamNG Lua mod fast — avoiding GC pressure, caching lookups, efficient string handling, and proper timer patterns.
BeamNG runs your onUpdate hook 60+ times per second. A single table allocation per frame means 3,600 garbage collections per minute. This guide shows how to write Lua that doesn't tank the game's framerate.
Per-Frame Allocation Avoidance
The #1 performance killer in Lua is garbage collection from per-frame table/string creation.
❌ Bad: Allocating in onUpdate
local function onUpdate(dtReal, dtSim, dtRaw)
local data = {x=1, y=2, z=3} -- NEW table every frame = GC pressure
local msg = "Speed: " .. tostring(speed) .. " m/s" -- String concat = allocation
guihooks.trigger('SpeedUpdate', {speed = speed}) -- NEW table every frame
end✅ Good: Reuse tables, avoid string concat in hot paths
local reuseData = {x=0, y=0, z=0} -- Allocated ONCE at load
local reuseStream = {speed = 0}
local function onUpdate(dtReal, dtSim, dtRaw)
reuseData.x, reuseData.y, reuseData.z = getPos()
reuseStream.speed = speed
guihooks.queueStream('myModSpeed', reuseStream) -- Reused table
endonUpdate vs onPreRender
| Hook | Frequency | Use For | Cost |
|---|---|---|---|
onUpdate | ~60Hz (frame rate) | Game logic, timers, state machines | Low |
onPreRender | ~60Hz (after physics) | Visual updates, debug drawing | Low |
onGuiUpdate | ~60Hz (during preRender) | UI-specific updates | Low |
Note: There is no onPhysicsStep hook in GE. Physics-rate callbacks exist only in the VE context.
String Operations
-- ❌ Bad: String concatenation in loops
local result = ""
for _, v in pairs(data) do
result = result .. v .. ", " -- Each .. creates a new string
end
-- ✅ Good: Use table.concat
local parts = {}
for _, v in pairs(data) do
parts[#parts+1] = v
end
local result = table.concat(parts, ", ")
-- ✅ Good: Use string.format for fixed patterns
log('I', 'mymod', string.format("Vehicle %d at (%.1f, %.1f)", vid, x, y))Table Size Checks
-- ❌ Bad: tableSize() iterates everything
if tableSize(myTable) > 0 then ...
-- ✅ Good: Use next() for emptiness check
if next(myTable) then ...
-- ✅ Good: Use tableIsEmpty() (checks type + next)
if not tableIsEmpty(myTable) then ...
-- ✅ Good: For arrays, use #
if #myArray > 0 then ...Caching Expensive Lookups
-- ❌ Bad: SceneTree lookup every frame
local function onUpdate(dt)
local obj = scenetree.findObject("myTrigger") -- String hash + lookup each frame
if obj then obj:doSomething() end
end
-- ✅ Good: Cache on level load
local myTrigger = nil
local function onClientPostStartMission()
myTrigger = scenetree.findObject("myTrigger")
end
local function onUpdate(dt)
if myTrigger then myTrigger:doSomething() end
endAvoid deepcopy in Hot Paths
-- ❌ Bad: deepcopy per frame
local function onUpdate(dt)
local snapshot = deepcopy(gameState) -- Recursive copy = slow
end
-- ✅ Good: Copy only what changed
local function onUpdate(dt)
cachedSpeed = gameState.speed -- Copy value, not table
endTimer Patterns
-- ❌ Bad: Frame counting (framerate-dependent)
local frames = 0
local function onUpdate()
frames = frames + 1
if frames % 60 == 0 then doThing() end -- Not reliable!
end
-- ✅ Good: Time accumulation
local timer = 0
local function onUpdate(dtReal, dtSim, dtRaw)
timer = timer + dtReal
if timer >= 1.0 then -- Every 1 second
timer = timer - 1.0
doThing()
end
endLocal Variable Upvalues
-- ✅ Good: Localize frequently-used globals
local min = math.min
local max = math.max
local floor = math.floor
local function onUpdate(dt)
local x = min(a, max(b, c)) -- Faster than math.min / math.max
endThis is standard Lua optimization - local variable access is faster than global table lookups.
Summary
| Pattern | Impact | How |
|---|---|---|
| Reuse tables | High | Module-level local tables, clear and refill |
| Avoid string concat | High | string.format or table.concat |
| Cache lookups | Medium | Store scenetree/map results at level load |
Use next() not tableSize() | Medium | if next(t) then for emptiness |
| Localize globals | Low-Medium | local min = math.min at top of file |
| Use throttled timers | Medium | Don't do expensive work every frame |
| Time-based timers | Correctness | Accumulate dtReal, not frame count |
See Also
- Anti-Patterns - Common mistakes to avoid
- Common Patterns - Proven extension patterns
- Hook Catalog - Understanding per-frame hooks