Why is optimization important in game development?
In games, every frame counts. If your code takes too long to execute, the game will start to lag — the FPS will drop, animations will twitch, and players will leave to write negative reviews. Optimization is especially critical in mobile games, where device resources are limited.
The good news is that Lua is already a fairly fast language, especially with the LuaJIT JIT compiler. But there are typical mistakes that can turn a fast game into a slideshow.
1. Local variables are your best friend
Problem: global variables in Lua run slower than local ones. Each time you access a global variable, the engine looks for it in a special table.
Bad:
function updatePlayer()
player.x = player.x + speed * deltaTime
player.y = player.y + gravity * deltaTime
endGood:
local function updatePlayer()
local px = player.x
local py = player.y
local spd = speed
local grav = gravity
local dt = deltaTime
px = px + spd * dt
py = py + grav * dt
player.x = px
player.y = py
endLocal variables are stored in registers or in a stack, access to them is much faster. This is especially important for functions that are called every frame (update, render).
2. Cache the calculation results
If a function returns the same value, do not call it again each time.
Bad:
function draw()
for i = 1, #enemies do
if distance(player, enemies[i]) < 100 then
drawEnemy(enemies[i])
end
end
endGood:
function draw()
local enemyCount = #enemies
local playerX, playerY = player.x, player.y
for i = 1, enemyCount do
local enemy = enemies[i]
local dx = enemy.x - playerX
local dy = enemy.y - playerY
if dx*dx + dy*dy < 10000 then -- избегаем sqrt
drawEnemy(enemy)
end
end
endHere we have hashed the number of enemies, the coordinates of the player and even avoided calling the distance function, replacing it with a comparison of squares of distances.
3. Avoid creating objects in loops
Creating new tables and rows is an expensive operation. The garbage collector then spends time cleaning them.
Bad:
function update()
for i = 1, 1000 do
local temp = {x = 0, y = 0} -- каждый кадр 1000 новых таблиц!
processData(temp)
end
endGood:
local tempData = {x = 0, y = 0} -- создали один раз
function update()
for i = 1, 1000 do
tempData.x = 0
tempData.y = 0
processData(tempData)
end
endOr use object pools for frequently created entities (pools, particles, enemies).
4. Correct work with strings
Concatenation of strings through .. in loops is a classic error.
Bad:
local result = ""
for i = 1, 1000 do
result = result .. tostring(i) .. "," -- O(n²) сложность!
endGood:
local parts = {}
for i = 1, 1000 do
parts[i] = tostring(i)
end
local result = table.concat(parts, ",") -- O(n) сложность5. Cycle optimization
The order in which conditions are checked matters. Put the most likely conditions first.
Bad:
for i = 1, #entities do
if entities[i].isDead and entities[i].isVisible and entities[i].isEnemy then
removeEntity(entities[i])
end
endGood:
for i = 1, #entities do
local entity = entities[i]
if entity.isEnemy and entity.isVisible and entity.isDead then
removeEntity(entity)
end
endIf most entities are not enemies, checking isEnemy first will save checking the rest of the conditions.

6. Use built-in functions
Built-in Lua functions are optimized at the C level and run faster than your implementations.
Bad:
function findMax(arr)
local max = arr[1]
for i = 2, #arr do
if arr[i] > max then
max = arr[i]
end
end
return max
endGood:
local max = math.max(unpack(scores)) -- для небольших массивов7. Profile the code
Don't optimize blindly! Use profilers to find real bottlenecks:
Love2D: built-in
love.graphics.getStats()Defold: built-in profiler
Roblox: MicroProfiler
It often turns out that the problem is not where you thought.
8. Practical tips for games
Spatial partitioning
Do not check collisions of all objects with all — this is O(n²). Use quadtrees or grids.
-- Вместо:
for i = 1, #bullets do
for j = 1, #enemies do
checkCollision(bullets[i], enemies[j])
end
end
-- Используйте пространственную сетку
local grid = createGrid(mapWidth, mapHeight, cellSize)
-- проверяйте коллизии только в соседних ячейкахLazy evaluation
Do not update objects that are not visible on the screen.
function updateEnemy(enemy)
if not isOnScreen(enemy) and distanceToPlayer(enemy) > 500 then
return -- враг далеко и не виден — пропускаем
end
-- обычная логика обновления
endRendering batching
Draw similar objects with one call if the engine supports it.
-- Вместо 100 отдельных вызовов draw:
local batch = love.graphics.newSpriteBatch(texture, 100)
for i = 1, #sprites do
batch:add(sprites[i].quad, sprites[i].x, sprites[i].y)
end
batch:draw()When not to optimize
Remember the rule: Premature optimization is the root of all evil. First write working code, then measure performance, and only if there are problems — optimize bottlenecks.
Code readability is more important than micro-optimizations. If the game runs at 60 FPS, you don't need to squeeze 120 FPS out of it at the cost of code clarity.
Conclusion
Optimizing Lua code for games is a balance between performance and readability. Follow these simple rules:
Use local variables
Cache calculations
Avoid creating waste
Profile before optimization
Apply algorithmic optimization (spatial partitioning, batching)
With this knowledge, your games will run quickly and smoothly even on weak devices!
All this and much more can be mastered in Codice — our educational platform for beginner developers. We create easy-to-understand courses in Python, JavaScript, Lua, and other programming languages.
And we also have cool Telegram channel with a friendly developer community! Communicate with like-minded people, ask questions, share your projects and get advice from experienced programmers.
Join us — it's more fun to learn together!
