roblox-performance

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Roblox Performance Optimization

Roblox 性能优化

Quick Reference

快速参考

TechniqueImpactWhen to Use
StreamingEnabledHighLarge open worlds
Object poolingHighFrequent spawn/destroy
Cache references outside loopsHighHeartbeat/RenderStepped
task.wait()
over
wait()
MediumAll scripts
MeshParts over UnionsMediumMany unique shapes
LOD (hide at distance)MediumComplex models
Anchor static partsMediumReduce physics budget
Limit PointLightsHighAny scene with many lights

优化技术影响程度适用场景
StreamingEnabled大型开放世界
对象池频繁生成/销毁对象的场景
在循环外缓存引用使用Heartbeat/RenderStepped的场景
使用
task.wait()
替代
wait()
所有脚本
使用MeshPart替代Union存在大量独特形状的场景
LOD(远距离隐藏)复杂模型
锚定静态部件减少物理计算开销
限制PointLight数量存在大量光源的场景

StreamingEnabled

StreamingEnabled

Enable for large worlds — engine sends only nearby parts to the client.
lua
-- Studio: Workspace > StreamingEnabled = true
workspace.StreamingEnabled = true
workspace.StreamingMinRadius = 64
workspace.StreamingTargetRadius = 128
  • Parts outside the radius are
    nil
    on the client — always guard with
    if part then
    .
  • Set
    Model.LevelOfDetail = Disabled
    on models that must always be present.
  • Pre-stream an area before a cutscene or teleport:
lua
workspace:RequestStreamAroundAsync(targetPosition, 5) -- 5s timeout

在大型开放世界中启用——引擎只会将客户端附近的部件发送给客户端。
lua
-- Studio: Workspace > StreamingEnabled = true
workspace.StreamingEnabled = true
workspace.StreamingMinRadius = 64
workspace.StreamingTargetRadius = 128
  • 超出半径的部件在客户端上为
    nil
    ——务必用
    if part then
    进行判断。
  • 对于必须始终显示的模型,设置
    Model.LevelOfDetail = Disabled
  • 在过场动画或传送前预加载指定区域:
lua
workspace:RequestStreamAroundAsync(targetPosition, 5) -- 5秒超时

Hot-Path Loop Optimization

热路径循环优化

RunService.Heartbeat
and
RenderStepped
fire every frame (~60×/sec). Keep them lean.
RunService.Heartbeat
RenderStepped
每帧触发一次(约60次/秒),需保持逻辑精简。

Bad — searching the hierarchy every frame

不良示例——每帧都搜索层级结构

lua
RunService.Heartbeat:Connect(function()
    local char = workspace:FindFirstChild(player.Name)
    local humanoid = char and char:FindFirstChild("Humanoid")
    if humanoid then humanoid.WalkSpeed = 16 end
end)
lua
RunService.Heartbeat:Connect(function()
    local char = workspace:FindFirstChild(player.Name)
    local humanoid = char and char:FindFirstChild("Humanoid")
    if humanoid then humanoid.WalkSpeed = 16 end
end)

Good — cache references once, do work only when needed

良好示例——仅缓存一次引用,仅在需要时执行操作

lua
local humanoid = nil

Players.LocalPlayer.CharacterAdded:Connect(function(char)
    humanoid = char:WaitForChild("Humanoid")
end)

RunService.Heartbeat:Connect(function(dt)
    if not humanoid then return end
    humanoid.WalkSpeed = 16  -- cached reference, no search
end)
Rules:
  • Cache
    game:GetService()
    and part references outside the loop.
  • Never call
    FindFirstChild
    ,
    GetChildren
    , or
    GetDescendants
    inside Heartbeat.
  • Throttle work that doesn't need every frame:
lua
local TICK_INTERVAL = 0.5
local elapsed = 0

RunService.Heartbeat:Connect(function(dt)
    elapsed += dt
    if elapsed < TICK_INTERVAL then return end
    elapsed = 0
    -- expensive work here, runs 2×/sec instead of 60×/sec
end)

lua
local humanoid = nil

Players.LocalPlayer.CharacterAdded:Connect(function(char)
    humanoid = char:WaitForChild("Humanoid")
end)

RunService.Heartbeat:Connect(function(dt)
    if not humanoid then return end
    humanoid.WalkSpeed = 16  -- 使用缓存的引用,无需搜索
end)
规则:
  • 在循环外缓存
    game:GetService()
    和部件引用。
  • 绝不在Heartbeat中调用
    FindFirstChild
    GetChildren
    GetDescendants
  • 对无需每帧执行的操作进行节流:
lua
local TICK_INTERVAL = 0.5
local elapsed = 0

RunService.Heartbeat:Connect(function(dt)
    elapsed += dt
    if elapsed < TICK_INTERVAL then return end
    elapsed = 0
    -- 此处执行高开销操作,每秒仅运行2次而非60次
end)

task Library vs Legacy Scheduler

task库 vs 旧版调度器

Always use
task
wait()
and
spawn()
throttle under load and are deprecated.
LegacyModern
wait(n)
task.wait(n)
spawn(fn)
task.spawn(fn)
delay(n, fn)
task.delay(n, fn)
coroutine.wrap(fn)()
task.spawn(fn)

始终使用
task
——
wait()
spawn()
在高负载下会被节流,且已被弃用。
旧版API新版API
wait(n)
task.wait(n)
spawn(fn)
task.spawn(fn)
delay(n, fn)
task.delay(n, fn)
coroutine.wrap(fn)()
task.spawn(fn)

Object Pooling

对象池

Reuse instances instead of creating and destroying them every frame.
lua
-- ObjectPool ModuleScript
local ObjectPool = {}
ObjectPool.__index = ObjectPool

function ObjectPool.new(template, initialSize)
    local self = setmetatable({ _template = template, _available = {} }, ObjectPool)
    for i = 1, initialSize do
        local obj = template:Clone()
        obj.Parent = nil
        table.insert(self._available, obj)
    end
    return self
end

function ObjectPool:Get(parent)
    local obj = table.remove(self._available) or self._template:Clone()
    obj.Parent = parent
    return obj
end

function ObjectPool:Return(obj)
    obj.Parent = nil
    table.insert(self._available, obj)
end

return ObjectPool
lua
-- Usage
local pool = ObjectPool.new(ReplicatedStorage.Bullet, 20)

local function fireBullet(origin)
    local bullet = pool:Get(workspace)
    bullet.CFrame = CFrame.new(origin)
    task.delay(3, function() pool:Return(bullet) end)
end

复用实例而非每帧创建和销毁对象。
lua
-- ObjectPool ModuleScript
local ObjectPool = {}
ObjectPool.__index = ObjectPool

function ObjectPool.new(template, initialSize)
    local self = setmetatable({ _template = template, _available = {} }, ObjectPool)
    for i = 1, initialSize do
        local obj = template:Clone()
        obj.Parent = nil
        table.insert(self._available, obj)
    end
    return self
end

function ObjectPool:Get(parent)
    local obj = table.remove(self._available) or self._template:Clone()
    obj.Parent = parent
    return obj
end

function ObjectPool:Return(obj)
    obj.Parent = nil
    table.insert(self._available, obj)
end

return ObjectPool
lua
-- 使用示例
local pool = ObjectPool.new(ReplicatedStorage.Bullet, 20)

local function fireBullet(origin)
    local bullet = pool:Get(workspace)
    bullet.CFrame = CFrame.new(origin)
    task.delay(3, function() pool:Return(bullet) end)
end

Level of Detail (LOD)

细节层级(LOD)

Built-in: Set
Model.LevelOfDetail = Automatic
— engine merges distant parts into an imposter mesh automatically.
Manual distance-based LOD:
lua
-- LocalScript
local INTERVAL = 0.2
local LOD_DISTANCE = 150
local elapsed = 0

RunService.Heartbeat:Connect(function(dt)
    elapsed += dt
    if elapsed < INTERVAL then return end
    elapsed = 0

    local dist = (workspace.CurrentCamera.CFrame.Position - model.PrimaryPart.Position).Magnitude
    local visible = dist < LOD_DISTANCE
    for _, v in model:GetDescendants() do
        if v:IsA("BasePart") then
            v.LocalTransparencyModifier = visible and 0 or 1
        end
    end
end)

内置LOD: 设置
Model.LevelOfDetail = Automatic
——引擎会自动将远处的部件合并为 impostor 网格。
基于距离的手动LOD:
lua
-- LocalScript
local INTERVAL = 0.2
local LOD_DISTANCE = 150
local elapsed = 0

RunService.Heartbeat:Connect(function(dt)
    elapsed += dt
    if elapsed < INTERVAL then return end
    elapsed = 0

    local dist = (workspace.CurrentCamera.CFrame.Position - model.PrimaryPart.Position).Magnitude
    local visible = dist < LOD_DISTANCE
    for _, v in model:GetDescendants() do
        if v:IsA("BasePart") then
            v.LocalTransparencyModifier = visible and 0 or 1
        end
    end
end)

Reducing Draw Calls

减少绘制调用

  • Merge parts that share a material into one MeshPart (export from Blender as
    .fbx
    ).
  • MeshParts batch better than CSG Unions (Unions re-triangulate at runtime).
  • Reuse materials — 10 parts sharing
    SmoothPlastic
    costs far less than 10 unique textures.
  • Use
    TextureId
    on a single MeshPart instead of stacking Decals on many parts.

  • 将使用相同材质的部件合并为单个MeshPart(从Blender导出为
    .fbx
    格式)。
  • MeshPart的批处理性能远优于CSG Union(Union会在运行时重新三角化)。
  • 复用材质——10个使用
    SmoothPlastic
    的部件开销远低于10个使用独特纹理的部件。
  • 在单个MeshPart上使用
    TextureId
    ,而非在多个部件上叠加Decal。

Profiling with MicroProfiler

使用MicroProfiler进行性能分析

  1. Press
    Ctrl+F6
    in-game to open MicroProfiler.
  2. Press
    Ctrl+P
    to pause and inspect a single frame.
  3. Look for wide bars in
    heartbeatSignal
    (Lua),
    physicsStepped
    (physics), or
    render
    (GPU).
  4. Label your own code:
lua
RunService.Heartbeat:Connect(function()
    debug.profilebegin("MySystem")
    -- your code
    debug.profileend()
end)

  1. 在游戏中按
    Ctrl+F6
    打开MicroProfiler。
  2. Ctrl+P
    暂停并检查单帧情况。
  3. 查看
    heartbeatSignal
    (Lua逻辑)、
    physicsStepped
    (物理计算)或
    render
    (GPU)中的宽条形块(代表高开销)。
  4. 为自己的代码添加标签:
lua
RunService.Heartbeat:Connect(function()
    debug.profilebegin("MySystem")
    -- 你的代码
    debug.profileend()
end)

Common FPS Killers

常见FPS杀手

CauseFix
Thousands of individual partsMerge into MeshParts
Unanchored static geometry
Anchored = true
on anything that never moves
Many
PointLight
/
SpotLight
instances
Limit to ~10–20 dynamic lights per area
High-rate ParticleEmittersLower
Rate
and
Lifetime
; disable when off-screen
wait()
under heavy load
Replace with
task.wait()
FindFirstChild
chains inside Heartbeat
Cache on load
StreamingEnabled off on large mapsEnable it
Model.LevelOfDetail = Disabled
everywhere
Use
Automatic
where safe

原因修复方案
数千个独立部件合并为MeshPart
未锚定的静态几何体对任何不会移动的部件设置
Anchored = true
大量
PointLight
/
SpotLight
实例
每个区域限制在10-20个动态光源以内
高频率的ParticleEmitters降低
Rate
Lifetime
;在粒子源不可见时禁用
高负载下使用
wait()
替换为
task.wait()
在Heartbeat中链式调用
FindFirstChild
在加载时缓存引用
大型地图未启用StreamingEnabled启用该功能
所有模型都设置
Model.LevelOfDetail = Disabled
在安全的场景下使用
Automatic

Common Mistakes

常见错误

MistakeFix
workspace:FindFirstChild
every frame
Cache reference on character/model load
Destroying and re-creating bullets/effectsUse an object pool
wait()
in tight loops
task.wait()
All parts with unique materialsStandardize to a small set of shared materials
ParticleEmitters enabled off-screenDisable
Enabled
when particle source is not visible
Physics on decorative parts
Anchored = true
错误做法修复方案
每帧调用
workspace:FindFirstChild
在角色/模型加载时缓存引用
销毁并重新创建子弹/特效使用对象池
在紧凑循环中使用
wait()
替换为
task.wait()
所有部件都使用独特材质标准化为少量共享材质
粒子源不可见时仍启用ParticleEmitters当粒子源不可见时禁用
Enabled
装饰性部件启用物理效果设置
Anchored = true