optimization

SKILL.md

Roblox Performance Optimization

When optimizing games, follow these patterns for better performance across all devices.

Rendering Optimization

Part Count Reduction

-- Combine multiple parts into unions or meshes
local function combineStaticParts(model)
    local parts = {}
    for _, part in ipairs(model:GetDescendants()) do
        if part:IsA("BasePart") and part.Anchored then
            table.insert(parts, part)
        end
    end

    if #parts > 1 then
        local union = parts[1]:UnionAsync(parts, Enum.CollisionFidelity.Box)
        union.Name = model.Name .. "_Combined"
        union.Parent = model.Parent

        for _, part in ipairs(parts) do
            part:Destroy()
        end

        return union
    end
end

-- Better: Use MeshPart for complex static geometry
-- Import optimized meshes from Blender with proper LODs

Level of Detail (LOD)

local LODManager = {}
local LOD_DISTANCES = {50, 100, 200}  -- Distance thresholds

function LODManager.setup(model)
    local lodLevels = {
        model:FindFirstChild("LOD0"),  -- Highest detail
        model:FindFirstChild("LOD1"),
        model:FindFirstChild("LOD2"),
        model:FindFirstChild("LOD3")   -- Lowest detail
    }

    local function updateLOD()
        local camera = workspace.CurrentCamera
        local distance = (model.PrimaryPart.Position - camera.CFrame.Position).Magnitude

        local activeLOD = 1
        for i, threshold in ipairs(LOD_DISTANCES) do
            if distance > threshold then
                activeLOD = i + 1
            end
        end

        for i, lod in ipairs(lodLevels) do
            if lod then
                lod.Visible = (i == activeLOD)
            end
        end
    end

    RunService.RenderStepped:Connect(updateLOD)
end

-- Automatic LOD using Roblox's built-in system
local function setupAutomaticLOD(meshPart)
    -- RenderFidelity controls automatic LOD
    meshPart.RenderFidelity = Enum.RenderFidelity.Automatic

    -- CollisionFidelity affects physics performance
    meshPart.CollisionFidelity = Enum.CollisionFidelity.Box  -- Simplest
end

Streaming Enabled

-- Enable instance streaming for large worlds
workspace.StreamingEnabled = true
workspace.StreamingMinRadius = 64    -- Min loaded radius
workspace.StreamingTargetRadius = 256 -- Target loaded radius
workspace.StreamingIntegrityMode = Enum.StreamingIntegrityMode.Default

-- For important models that must always be loaded
local importantModel = workspace.ImportantModel
importantModel.ModelStreamingMode = Enum.ModelStreamingMode.Atomic -- Load together
-- or
importantModel.ModelStreamingMode = Enum.ModelStreamingMode.Persistent -- Always loaded

Texture Optimization

-- Use appropriate texture sizes
-- Mobile: 256x256 or 512x512
-- Desktop: 512x512 or 1024x1024 max

-- Reduce unique materials
local function consolidateMaterials(model)
    local materials = {}
    for _, part in ipairs(model:GetDescendants()) do
        if part:IsA("BasePart") then
            local key = tostring(part.Material) .. "_" .. tostring(part.Color)
            materials[key] = (materials[key] or 0) + 1
        end
    end
    -- Identify and consolidate similar materials
end

Script Optimization

Avoid wait() and Use task Library

-- BAD: Uses deprecated wait()
wait(1)
spawn(function() ... end)
delay(1, function() ... end)

-- GOOD: Use task library
task.wait(1)
task.spawn(function() ... end)
task.delay(1, function() ... end)

-- Even better: Use events when possible
part.Touched:Wait()  -- Yields until event fires

Event Connection Management

-- BAD: Memory leak - connection never disconnected
part.Touched:Connect(function()
    -- This connection persists even after part is destroyed
end)

-- GOOD: Store and disconnect connections
local connection
connection = part.Touched:Connect(function(hit)
    if someCondition then
        connection:Disconnect()
    end
end)

-- BEST: Use Maid/Janitor pattern for cleanup
local Maid = {}
Maid.__index = Maid

function Maid.new()
    return setmetatable({_tasks = {}}, Maid)
end

function Maid:GiveTask(task)
    table.insert(self._tasks, task)
end

function Maid:Cleanup()
    for _, task in ipairs(self._tasks) do
        if typeof(task) == "RBXScriptConnection" then
            task:Disconnect()
        elseif typeof(task) == "Instance" then
            task:Destroy()
        elseif type(task) == "function" then
            task()
        end
    end
    self._tasks = {}
end

Caching and Avoiding Repeated Lookups

-- BAD: Repeated FindFirstChild every frame
RunService.Heartbeat:Connect(function()
    local hrp = player.Character:FindFirstChild("HumanoidRootPart")
    local humanoid = player.Character:FindFirstChildOfClass("Humanoid")
    -- ...
end)

-- GOOD: Cache references
local character, hrp, humanoid

local function cacheCharacter()
    character = player.Character
    if character then
        hrp = character:WaitForChild("HumanoidRootPart")
        humanoid = character:WaitForChild("Humanoid")
    end
end

player.CharacterAdded:Connect(cacheCharacter)
cacheCharacter()

RunService.Heartbeat:Connect(function()
    if hrp then
        -- Use cached reference
    end
end)

Table Operations

-- BAD: Creating new tables constantly
RunService.Heartbeat:Connect(function()
    local data = {x = 1, y = 2, z = 3}  -- New table every frame
end)

-- GOOD: Reuse tables
local data = {x = 0, y = 0, z = 0}
RunService.Heartbeat:Connect(function()
    data.x, data.y, data.z = 1, 2, 3
end)

-- Use table.create for known sizes
local arr = table.create(1000, 0)  -- Pre-allocate 1000 slots

-- Clear table without creating new one
local function clearTable(t)
    for k in pairs(t) do
        t[k] = nil
    end
end

Local vs Global Variables

-- BAD: Accessing globals is slower
for i = 1, 1000000 do
    local x = math.sin(i)  -- Global lookup each time
end

-- GOOD: Cache in local variable
local sin = math.sin
for i = 1, 1000000 do
    local x = sin(i)  -- Local lookup is faster
end

Memory Optimization

Instance Destruction

-- Properly destroy instances to free memory
local function cleanup(instance)
    -- Disconnect all connections first
    for _, connection in ipairs(instance:GetConnections()) do
        connection:Disconnect()
    end

    -- Clear attributes
    for _, attr in ipairs(instance:GetAttributes()) do
        instance:SetAttribute(attr, nil)
    end

    instance:Destroy()
end

-- Nil references after destroy
local part = Instance.new("Part")
part:Destroy()
part = nil  -- Allow garbage collection

Object Pooling

local ObjectPool = {}
ObjectPool.__index = ObjectPool

function ObjectPool.new(template, initialSize)
    local pool = setmetatable({
        template = template,
        available = {},
        active = {}
    }, ObjectPool)

    for i = 1, initialSize do
        local obj = template:Clone()
        obj.Parent = nil
        table.insert(pool.available, obj)
    end

    return pool
end

function ObjectPool:get()
    local obj = table.remove(self.available)
    if not obj then
        obj = self.template:Clone()
    end
    table.insert(self.active, obj)
    return obj
end

function ObjectPool:release(obj)
    local index = table.find(self.active, obj)
    if index then
        table.remove(self.active, index)
    end
    obj.Parent = nil  -- Remove from world
    -- Reset state...
    table.insert(self.available, obj)
end

Garbage Collection Awareness

-- Avoid creating garbage in hot loops
-- BAD:
RunService.Heartbeat:Connect(function()
    local info = {  -- Creates garbage every frame
        position = hrp.Position,
        velocity = hrp.AssemblyLinearVelocity
    }
end)

-- GOOD: Use primitives or reuse tables
local cachedPosition = Vector3.new()
local cachedVelocity = Vector3.new()

RunService.Heartbeat:Connect(function()
    -- Vectors are value types, no garbage created
    local pos = hrp.Position
    local vel = hrp.AssemblyLinearVelocity
end)

-- Manual GC control (use sparingly)
local function forceGC()
    collectgarbage("collect")
end

Physics Optimization

Collision Groups

local PhysicsService = game:GetService("PhysicsService")

-- Create collision groups
PhysicsService:RegisterCollisionGroup("Players")
PhysicsService:RegisterCollisionGroup("Enemies")
PhysicsService:RegisterCollisionGroup("Projectiles")
PhysicsService:RegisterCollisionGroup("Debris")

-- Disable unnecessary collisions
PhysicsService:CollisionGroupSetCollidable("Players", "Players", false)
PhysicsService:CollisionGroupSetCollidable("Projectiles", "Projectiles", false)
PhysicsService:CollisionGroupSetCollidable("Debris", "Debris", false)

-- Assign to parts
local function setCollisionGroup(part, groupName)
    part.CollisionGroup = groupName
end

Anchored Parts

-- Anchor static parts to remove from physics simulation
local function optimizeStaticParts(model)
    for _, part in ipairs(model:GetDescendants()) do
        if part:IsA("BasePart") then
            local isStatic = not part:FindFirstChildOfClass("Motor6D")
                         and not part:FindFirstChildOfClass("Weld")
            if isStatic then
                part.Anchored = true
            end
        end
    end
end

Simplified Collision

-- Use simpler collision shapes
meshPart.CollisionFidelity = Enum.CollisionFidelity.Box  -- Fastest
meshPart.CollisionFidelity = Enum.CollisionFidelity.Hull -- Medium
meshPart.CollisionFidelity = Enum.CollisionFidelity.Default -- Detailed

-- Disable collisions for visual-only parts
visualPart.CanCollide = false
visualPart.CanQuery = false  -- Excludes from raycasts too
visualPart.CanTouch = false  -- Excludes from Touched events

Network Optimization

Minimize RemoteEvent Traffic

-- BAD: Fire every frame
RunService.Heartbeat:Connect(function()
    PositionRemote:FireServer(hrp.Position)
end)

-- GOOD: Throttle updates
local lastUpdate = 0
local UPDATE_RATE = 1/20  -- 20 updates per second

RunService.Heartbeat:Connect(function()
    local now = os.clock()
    if now - lastUpdate >= UPDATE_RATE then
        lastUpdate = now
        PositionRemote:FireServer(hrp.Position)
    end
end)

-- BETTER: Only send when changed significantly
local lastSentPosition = Vector3.new()
local POSITION_THRESHOLD = 0.5

RunService.Heartbeat:Connect(function()
    local pos = hrp.Position
    if (pos - lastSentPosition).Magnitude > POSITION_THRESHOLD then
        lastSentPosition = pos
        PositionRemote:FireServer(pos)
    end
end)

Data Compression

-- Quantize positions to reduce data size
local function quantizeVector3(v, precision)
    precision = precision or 0.1
    return Vector3.new(
        math.floor(v.X / precision) * precision,
        math.floor(v.Y / precision) * precision,
        math.floor(v.Z / precision) * precision
    )
end

-- Pack multiple values
local function packColor(color)
    return color.R * 65536 + color.G * 256 + color.B
end

local function unpackColor(packed)
    local r = math.floor(packed / 65536)
    local g = math.floor((packed % 65536) / 256)
    local b = packed % 256
    return Color3.fromRGB(r, g, b)
end

Profiling Tools

MicroProfiler

-- Use debug.profilebegin/end for custom profiling
debug.profilebegin("MyExpensiveFunction")
-- ... expensive code ...
debug.profileend()

-- View in MicroProfiler (Ctrl+F6 in Studio)

Performance Stats

local Stats = game:GetService("Stats")

local function logPerformance()
    print("Memory:", Stats:GetTotalMemoryUsageMb(), "MB")
    print("Instances:", Stats.InstanceCount)
    print("Data Receive:", Stats.DataReceiveKbps, "Kbps")
    print("Data Send:", Stats.DataSendKbps, "Kbps")
    print("Physics Step:", Stats.PhysicsStepTimeMs, "ms")
end

Frame Rate Monitoring

local frameCount = 0
local lastTime = os.clock()

RunService.RenderStepped:Connect(function()
    frameCount = frameCount + 1

    local now = os.clock()
    if now - lastTime >= 1 then
        local fps = frameCount / (now - lastTime)
        print("FPS:", math.floor(fps))
        frameCount = 0
        lastTime = now
    end
end)
Weekly Installs
2
Installed on
codex2
claude-code2
windsurf1
opencode1
cursor1
antigravity1