roblox-performance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRoblox Performance
Roblox 性能优化
Use this skill when profiling, diagnosing lag, optimizing code, or setting performance budgets.
当你需要进行性能分析、诊断卡顿、优化代码或设置性能预算时,可以使用本技能。
Performance Targets
性能指标目标
Server
服务器端
| Metric | Target | Hard 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 budget | 60 + (players × 10) req/min | per Get/Set type |
| 指标 | 目标值 | 硬性上限 |
|---|---|---|
| 心跳帧时间 | < 16ms (60Hz) | < 33ms (30Hz) |
| 脚本执行时间 | < 10ms/帧 | < 20ms |
| 内存 | 典型值 < 2GB | 可用上限 ~3.5GB |
| 网络输出 | 单玩家 < 50KB/s | — |
| DataStore 请求预算 | 60 + (玩家数 × 10) 次/分钟 | 按Get/Set类型分别计算 |
Client
客户端
| Metric | Target | Minimum |
|---|---|---|
| FPS (desktop) | 60 | 30 |
| FPS (mobile) | 45 | 30 |
| Memory (mobile) | < 800MB | < 1.2GB (crash zone) |
| Memory (desktop) | < 1.5GB | < 3GB |
| Load time | < 10s to playable | < 20s |
| Input latency | < 100ms | < 200ms |
| 指标 | 目标值 | 最低要求 |
|---|---|---|
| 桌面端FPS | 60 | 30 |
| 移动端FPS | 45 | 30 |
| 移动端内存 | < 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
脚本问题
| Problem | Symptom | Fix |
|---|---|---|
| Heartbeat loop over many instances | Server frame time spike | Event-driven or batch with yielding |
| Repeated workspace lookups | Unnecessary overhead | Cache references in variables |
| Table allocation in hot paths | GC pressure, frame spikes | Reuse preallocated tables |
| String concatenation in loops | O(n²) allocation | |
| Signal over-subscription | Many listeners on one event | Batch or partition |
| Unthrottled RenderStepped | Client FPS drop | Only use for camera/input, throttle everything else |
| require() in loops | Repeated module resolution | Cache module reference outside loop |
| 问题 | 症状 | 修复方案 |
|---|---|---|
| 遍历大量实例的心跳循环 | 服务器帧时间突增 | 改用事件驱动或批量处理并加入yield |
| 重复查找Workspace | 不必要的性能开销 | 将引用缓存到变量中 |
| 热点路径中频繁创建表 | GC压力增大、帧时间突增 | 复用预分配的表 |
| 循环中拼接字符串 | O(n²)内存分配 | 使用 |
| 信号过度订阅 | 单个事件存在大量监听器 | 批量处理或分区订阅 |
| 未限流的RenderStepped | 客户端FPS下降 | 仅用于相机/输入处理,其他逻辑限流 |
| 循环中调用require() | 重复解析模块 | 在循环外缓存模块引用 |
Memory
内存问题
| Problem | Symptom | Fix |
|---|---|---|
| Undisconnected events | Memory grows over time | Trove/Maid pattern, disconnect on cleanup |
| Orphaned instances | Memory never freed | Destroy() instances, nil references |
| Large tables never cleared | Lua GC can't collect | Set to nil or use weak tables |
| Excessive cloning | Memory spikes on spawn | Object pooling |
| Uncompressed images | High texture memory | Use compressed formats, reduce resolution |
| 问题 | 症状 | 修复方案 |
|---|---|---|
| 未断开的事件连接 | 内存持续增长 | 使用Trove/Maid模式,清理时断开连接 |
| 孤立实例 | 内存无法释放 | 调用Destroy()销毁实例,置空引用 |
| 大型表从未清空 | Lua GC无法回收 | 置为nil或使用弱引用表 |
| 过度克隆 | 生成时内存突增 | 对象池化 |
| 未压缩图片 | 纹理内存占用过高 | 使用压缩格式,降低分辨率 |
Rendering
渲染问题
| Problem | Symptom | Fix |
|---|---|---|
| High part count | Low FPS, draw call bound | Merge static geometry, use MeshParts |
| Transparent part stacking | Overdraw, GPU bound | Reduce layers, use CanvasGroup for UI |
| Excessive particles | Mobile FPS death | Cap ParticleEmitter.Rate, reduce on mobile |
| Too many dynamic lights | Frame time spike | Limit to 4-6 active lights per area |
| Post-processing stacking | GPU overhead | One BloomEffect, one ColorCorrection max |
| 问题 | 症状 | 修复方案 |
|---|---|---|
| 部件数量过多 | FPS过低、受绘制调用限制 | 合并静态几何体,使用MeshParts |
| 透明部件堆叠 | 过度绘制、GPU负载过高 | 减少层数,UI使用CanvasGroup |
| 粒子效果过多 | 移动端FPS骤降 | 限制ParticleEmitter.Rate,移动端减少粒子数量 |
| 动态灯光过多 | 帧时间突增 | 每个区域限制4-6盏活跃灯光 |
| 后处理效果堆叠 | GPU开销过大 | 最多保留一个BloomEffect和一个ColorCorrection |
Network
网络问题
| Problem | Symptom | Fix |
|---|---|---|
| Frequent RemoteEvent fires | Bandwidth spike | Batch updates, throttle to 10-20/sec |
| Large payloads | Lag spike on fire | Send IDs not full objects, compress data |
| Replicating unnecessary instances | Join time slow | Keep Workspace lean, use ServerStorage |
| Unthrottled property changes | Network saturation | Batch 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)
endluau
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)
endThrottled 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
endluau
-- 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
endLazy 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
endluau
-- 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
endMobile-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 and their descendants stream in/out. Other instances (Folders, ValueObjects, RemoteEvents, ModuleScripts) load during initial join and never stream.
BasePartsWhen 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:
- — radius (studs) engine keeps loaded. Start at 256, tune down for mobile.
StreamingTargetRadius - — guaranteed radius. Set ~64 for nearby content.
StreamingMinRadius - — what happens during load (Default, Disabled, ClientPhysicsPause).
StreamingPauseMode
Gotcha: returns nil if the part is streamed out. Use with timeout, or design systems that don't depend on distant parts existing on the client.
workspace:FindFirstChild("DistantPart")WaitForChild新建场景默认启用StreamingEnabled。仅及其子对象会进行流式加载/卸载。其他实例(Folders、ValueObjects、RemoteEvents、ModuleScripts)会在初始加入时加载,且不会被流式卸载。
BaseParts当实例被流式卸载时,它们会被设置Parent为nil(而非销毁)。如果实例再次流式加载,Luau中的引用会保留。卸载信号会触发,但本地属性变更可能丢失。
配置项:
- — 引擎保持加载的半径(单位:studs),初始设置为256,针对移动端调低。
StreamingTargetRadius - — 保证加载的半径,设置为~64以确保附近内容加载。
StreamingMinRadius - — 加载期间的行为(Default、Disabled、ClientPhysicsPause)。
StreamingPauseMode
注意事项: 若部件已被流式卸载则返回nil。使用带超时的,或设计不依赖客户端存在远程部件的系统。
workspace:FindFirstChild("DistantPart")WaitForChildDetect 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
endluau
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
endPerformance 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