roblox-performance

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Roblox Performance

Roblox 性能优化

Use this skill when profiling, diagnosing lag, optimizing code, or setting performance budgets.
当你需要进行性能分析、诊断卡顿、优化代码或设置性能预算时,可以使用本技能。

Performance Targets

性能指标目标

Server

服务器端

MetricTargetHard Limit
Heartbeat time< 16ms (60Hz)< 33ms (30Hz)
Script time< 10ms/frame< 20ms
Memory< 2GB typical~3.5GB available
Network out< 50KB/s per player
DataStore budget60 + (players × 10) req/minper Get/Set type
指标目标值硬性上限
心跳帧时间< 16ms (60Hz)< 33ms (30Hz)
脚本执行时间< 10ms/帧< 20ms
内存典型值 < 2GB可用上限 ~3.5GB
网络输出单玩家 < 50KB/s
DataStore 请求预算60 + (玩家数 × 10) 次/分钟按Get/Set类型分别计算

Client

客户端

MetricTargetMinimum
FPS (desktop)6030
FPS (mobile)4530
Memory (mobile)< 800MB< 1.2GB (crash zone)
Memory (desktop)< 1.5GB< 3GB
Load time< 10s to playable< 20s
Input latency< 100ms< 200ms
指标目标值最低要求
桌面端FPS6030
移动端FPS4530
移动端内存< 800MB< 1.2GB(崩溃风险区间)
桌面端内存< 1.5GB< 3GB
加载时间< 10秒进入可玩状态< 20秒
输入延迟< 100ms< 200ms

Profiling Tools

性能分析工具

MicroProfiler (Ctrl+F6)

MicroProfiler (Ctrl+F6)

Per-frame breakdown of time spent in scripts, physics, rendering. The primary tool for finding what's actually slow.
  • Server: View → MicroProfiler
  • Client: Ctrl+Alt+F6 during playtest
  • Look for: long bars in "Script" category, physics spikes, render thread stalls
逐帧分解脚本、物理运算、渲染环节的耗时。这是定位真正性能瓶颈的核心工具。
  • 服务器端:View → MicroProfiler
  • 客户端:Playtest期间按Ctrl+Alt+F6
  • 重点关注:「Script」类别中的长条形耗时、物理运算峰值、渲染线程卡顿

Developer Console (F9)

Developer Console (F9)

  • Stats: Memory, network, render stats
  • Server Stats (game owner): Server-side metrics
  • Script Performance: Per-script CPU time
  • 统计面板:内存、网络、渲染统计数据
  • 服务器统计数据(仅游戏所有者可见):服务器端核心指标
  • 脚本性能:单脚本CPU耗时统计

Script Profiler (Ctrl+Alt+F5)

Script Profiler (Ctrl+Alt+F5)

  • Per-script CPU usage and heap allocations
  • Identifies which scripts are hot
  • 单脚本CPU占用率与堆内存分配情况
  • 定位热点脚本

Common Performance Issues

常见性能问题

Scripts

脚本问题

ProblemSymptomFix
Heartbeat loop over many instancesServer frame time spikeEvent-driven or batch with yielding
Repeated workspace lookupsUnnecessary overheadCache references in variables
Table allocation in hot pathsGC pressure, frame spikesReuse preallocated tables
String concatenation in loopsO(n²) allocation
table.concat()
Signal over-subscriptionMany listeners on one eventBatch or partition
Unthrottled RenderSteppedClient FPS dropOnly use for camera/input, throttle everything else
require() in loopsRepeated module resolutionCache module reference outside loop
问题症状修复方案
遍历大量实例的心跳循环服务器帧时间突增改用事件驱动或批量处理并加入yield
重复查找Workspace不必要的性能开销将引用缓存到变量中
热点路径中频繁创建表GC压力增大、帧时间突增复用预分配的表
循环中拼接字符串O(n²)内存分配使用
table.concat()
信号过度订阅单个事件存在大量监听器批量处理或分区订阅
未限流的RenderStepped客户端FPS下降仅用于相机/输入处理,其他逻辑限流
循环中调用require()重复解析模块在循环外缓存模块引用

Memory

内存问题

ProblemSymptomFix
Undisconnected eventsMemory grows over timeTrove/Maid pattern, disconnect on cleanup
Orphaned instancesMemory never freedDestroy() instances, nil references
Large tables never clearedLua GC can't collectSet to nil or use weak tables
Excessive cloningMemory spikes on spawnObject pooling
Uncompressed imagesHigh texture memoryUse compressed formats, reduce resolution
问题症状修复方案
未断开的事件连接内存持续增长使用Trove/Maid模式,清理时断开连接
孤立实例内存无法释放调用Destroy()销毁实例,置空引用
大型表从未清空Lua GC无法回收置为nil或使用弱引用表
过度克隆生成时内存突增对象池化
未压缩图片纹理内存占用过高使用压缩格式,降低分辨率

Rendering

渲染问题

ProblemSymptomFix
High part countLow FPS, draw call boundMerge static geometry, use MeshParts
Transparent part stackingOverdraw, GPU boundReduce layers, use CanvasGroup for UI
Excessive particlesMobile FPS deathCap ParticleEmitter.Rate, reduce on mobile
Too many dynamic lightsFrame time spikeLimit to 4-6 active lights per area
Post-processing stackingGPU overheadOne BloomEffect, one ColorCorrection max
问题症状修复方案
部件数量过多FPS过低、受绘制调用限制合并静态几何体,使用MeshParts
透明部件堆叠过度绘制、GPU负载过高减少层数,UI使用CanvasGroup
粒子效果过多移动端FPS骤降限制ParticleEmitter.Rate,移动端减少粒子数量
动态灯光过多帧时间突增每个区域限制4-6盏活跃灯光
后处理效果堆叠GPU开销过大最多保留一个BloomEffect和一个ColorCorrection

Network

网络问题

ProblemSymptomFix
Frequent RemoteEvent firesBandwidth spikeBatch updates, throttle to 10-20/sec
Large payloadsLag spike on fireSend IDs not full objects, compress data
Replicating unnecessary instancesJoin time slowKeep Workspace lean, use ServerStorage
Unthrottled property changesNetwork saturationBatch property changes, use attributes
问题症状修复方案
频繁触发RemoteEvent带宽突增批量更新,限流至10-20次/秒
大负载数据触发时卡顿发送ID而非完整对象,压缩数据
复制不必要的实例加入游戏时间过长保持Workspace精简,使用ServerStorage
未限流的属性变更网络饱和批量属性变更,使用attributes

Optimization Patterns

优化模式

Object Pooling

对象池化

luau
local Pool = {}
Pool.__index = Pool

function Pool.new(template: Instance, initialSize: number)
    local self = setmetatable({
        _template = template,
        _available = {},
        _active = {},
    }, Pool)

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

function Pool:get(): Instance
    local obj = table.remove(self._available)
    if not obj then
        obj = self._template:Clone()
    end
    self._active[obj] = true
    return obj
end

function Pool:release(obj: Instance)
    self._active[obj] = nil
    obj.Parent = nil
    -- Reset state here
    table.insert(self._available, obj)
end
luau
local Pool = {}
Pool.__index = Pool

function Pool.new(template: Instance, initialSize: number)
    local self = setmetatable({
        _template = template,
        _available = {},
        _active = {},
    }, Pool)

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

function Pool:get(): Instance
    local obj = table.remove(self._available)
    if not obj then
        obj = self._template:Clone()
    end
    self._active[obj] = true
    return obj
end

function Pool:release(obj: Instance)
    self._active[obj] = nil
    obj.Parent = nil
    -- Reset state here
    table.insert(self._available, obj)
end

Throttled Updates

限流更新

luau
-- Instead of updating every frame, batch at fixed intervals
local TICK_RATE = 1/10 -- 10 updates per second
local accumulated = 0

RunService.Heartbeat:Connect(function(dt)
    accumulated += dt
    if accumulated < TICK_RATE then return end
    accumulated -= TICK_RATE

    -- Do expensive work here (runs 10x/sec, not 60x)
    updateAllNPCs()
end)
luau
-- Instead of updating every frame, batch at fixed intervals
local TICK_RATE = 1/10 -- 10 updates per second
local accumulated = 0

RunService.Heartbeat:Connect(function(dt)
    accumulated += dt
    if accumulated < TICK_RATE then return end
    accumulated -= TICK_RATE

    -- Do expensive work here (runs 10x/sec, not 60x)
    updateAllNPCs()
end)

Spatial Partitioning

空间分区

luau
-- Don't check all entities against all entities
-- Use distance-based activation
local ACTIVATION_RANGE = 100

local function getActiveEntities(playerPosition: Vector3): {Instance}
    local active = {}
    for _, entity in allEntities do
        if (entity.Position - playerPosition).Magnitude < ACTIVATION_RANGE then
            table.insert(active, entity)
        end
    end
    return active
end
luau
-- Don't check all entities against all entities
-- Use distance-based activation
local ACTIVATION_RANGE = 100

local function getActiveEntities(playerPosition: Vector3): {Instance}
    local active = {}
    for _, entity in allEntities do
        if (entity.Position - playerPosition).Magnitude < ACTIVATION_RANGE then
            table.insert(active, entity)
        end
    end
    return active
end

Lazy Loading

懒加载

luau
-- Don't load everything at once
-- Stream content as player approaches
local loaded = {}

local function ensureLoaded(zoneName: string)
    if loaded[zoneName] then return end
    loaded[zoneName] = true

    local zone = ServerStorage.Zones:FindFirstChild(zoneName)
    if zone then
        zone:Clone().Parent = workspace.ActiveZones
    end
end
luau
-- Don't load everything at once
-- Stream content as player approaches
local loaded = {}

local function ensureLoaded(zoneName: string)
    if loaded[zoneName] then return end
    loaded[zoneName] = true

    local zone = ServerStorage.Zones:FindFirstChild(zoneName)
    if zone then
        zone:Clone().Parent = workspace.ActiveZones
    end
end

Mobile-Specific Optimization

移动端专属优化

Mobile is 60%+ of Roblox players. Optimize for it specifically:
  • Part count: Keep under 5000 visible parts. Use StreamingEnabled.
  • Textures: Max 512x512 for most textures. 1024 only for hero assets.
  • Particles: Cap at 50 total active emitters. Reduce Rate on mobile.
  • UI: Use CanvasGroup to batch UI rendering. Avoid deep nesting.
  • Shadows: Consider disabling GlobalShadows on mobile or reducing ShadowSoftness.
  • Draw distance: Reduce via StreamingEnabled MinRadius/TargetRadius.
移动端玩家占Roblox玩家总数的60%以上,需针对性优化:
  • 部件数量:可见部件保持在5000以下,启用StreamingEnabled。
  • 纹理:大多数纹理最大512x512,仅核心资源使用1024。
  • 粒子效果:活跃发射器总数限制在50以内,移动端降低Rate。
  • 界面:使用CanvasGroup批量渲染UI,避免深层嵌套。
  • 阴影:考虑在移动端禁用GlobalShadows或降低ShadowSoftness。
  • 绘制距离:通过StreamingEnabled的MinRadius/TargetRadius降低。

StreamingEnabled

StreamingEnabled

StreamingEnabled is on by default for new places. Only
BaseParts
and their descendants stream in/out. Other instances (Folders, ValueObjects, RemoteEvents, ModuleScripts) load during initial join and never stream.
When instances stream out, they are parented to nil (not destroyed). Luau references persist if they stream back in. Removal signals fire, but local-only property changes may be lost.
Configuration:
  • StreamingTargetRadius
    — radius (studs) engine keeps loaded. Start at 256, tune down for mobile.
  • StreamingMinRadius
    — guaranteed radius. Set ~64 for nearby content.
  • StreamingPauseMode
    — what happens during load (Default, Disabled, ClientPhysicsPause).
Gotcha:
workspace:FindFirstChild("DistantPart")
returns nil if the part is streamed out. Use
WaitForChild
with timeout, or design systems that don't depend on distant parts existing on the client.
新建场景默认启用StreamingEnabled。仅
BaseParts
及其子对象会进行流式加载/卸载。其他实例(Folders、ValueObjects、RemoteEvents、ModuleScripts)会在初始加入时加载,且不会被流式卸载。
当实例被流式卸载时,它们会被设置Parent为nil(而非销毁)。如果实例再次流式加载,Luau中的引用会保留。卸载信号会触发,但本地属性变更可能丢失。
配置项:
  • StreamingTargetRadius
    — 引擎保持加载的半径(单位:studs),初始设置为256,针对移动端调低。
  • StreamingMinRadius
    — 保证加载的半径,设置为~64以确保附近内容加载。
  • StreamingPauseMode
    — 加载期间的行为(Default、Disabled、ClientPhysicsPause)。
注意事项
workspace:FindFirstChild("DistantPart")
若部件已被流式卸载则返回nil。使用带超时的
WaitForChild
,或设计不依赖客户端存在远程部件的系统。

Detect Platform

检测平台

luau
local UserInputService = game:GetService("UserInputService")

local isMobile = UserInputService.TouchEnabled
    and not UserInputService.KeyboardEnabled

if isMobile then
    -- Reduce quality settings
    workspace.StreamingEnabled = true
    -- Reduce particle counts, disable expensive effects
end
luau
local UserInputService = game:GetService("UserInputService")

local isMobile = UserInputService.TouchEnabled
    and not UserInputService.KeyboardEnabled

if isMobile then
    -- Reduce quality settings
    workspace.StreamingEnabled = true
    -- Reduce particle counts, disable expensive effects
end

Performance Budget Template

性能预算模板

Set these before building, enforce during development:
SERVER BUDGET (per Heartbeat frame, 16ms total):
  Physics:     4ms
  Scripts:     8ms
  Replication: 2ms
  Overhead:    2ms

CLIENT BUDGET (per render frame, 16ms for 60fps):
  Render:      8ms
  Scripts:     4ms
  Physics:     2ms
  UI:          1ms
  Overhead:    1ms

MEMORY BUDGET:
  Mobile:      600MB max (leave headroom for OS)
  Desktop:     1.5GB max

NETWORK BUDGET:
  Per player:  30KB/s average, 100KB/s burst
  RemoteEvents: max 20 fires/sec per remote
在开发前设置这些预算,并在开发过程中严格执行:
SERVER BUDGET (每心跳帧,总计16ms):
  Physics:     4ms
  Scripts:     8ms
  Replication: 2ms
  Overhead:    2ms

CLIENT BUDGET (每渲染帧,60fps对应16ms):
  Render:      8ms
  Scripts:     4ms
  Physics:     2ms
  UI:          1ms
  Overhead:    1ms

MEMORY BUDGET:
  Mobile:      600MB max (为系统预留空间)
  Desktop:     1.5GB max

NETWORK BUDGET:
  Per player:  30KB/s average, 100KB/s burst
  RemoteEvents: max 20 fires/sec per remote