luau-roblox-optimizer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Luau Roblox Optimizer

Luau Roblox 优化器

Review checklist for scripts that live inside Roblox Studio. Every item names the symptom, the cause, and the one-line fix. Studio-only means: no external tooling, no file layout advice, no
--!strict
lectures. Script Analysis, the Output window, the Developer Console (F9), and MicroProfiler (Ctrl+F6) are the whole toolkit.
审查驻留在 Roblox Studio 内的脚本的检查清单。每项都会列出症状、原因和一行修复方案。仅限 Studio:不需要外部工具,不提供文件布局建议,也不进行
--!strict
说教。Script Analysis、Output 窗口、Developer Console(F9)和 MicroProfiler(Ctrl+F6)就是全部工具。

Review output

审查输出

A review IS, in this order:
  1. Exploits and data loss - anything a client can abuse or that drops saved data.
  2. Crashes - nil indexes, races, errors that stop the script.
  3. Leaks - connections, threads, and tables that outlive their owner.
  4. Performance - work done more often than needed.
  5. Animation and DataStore hygiene - the two areas below.
Each finding is one line:
line N - what breaks - fix
. Items that need the whole file to change (adopt a framework, restructure folders) are out of scope for a Studio review.
审查应按以下顺序进行:
  1. 可被利用的问题与数据丢失——客户端能够滥用,或导致已保存数据丢失的所有问题。
  2. 崩溃——nil 索引、竞态,以及会停止脚本的错误。
  3. 泄漏——生命周期超过其所有者的连接、线程和表。
  4. 性能——执行次数超过实际需要的任务。
  5. 动画与 DataStore 规范——下面两个领域。
每条发现占一行:
line N - what breaks - fix
。需要修改整个文件的项(采用框架、重构文件夹)不在 Studio 审查范围内。

Scan triggers

扫描触发条件

Walk the file once. Each token below opens a fixed set of checks; report every check that fails, even when the same line already has a worse finding.
SeeingCheck
Touched
debounce,
hit
may be an Accessory handle, connection cleaned when the part goes
OnServerEvent
,
OnServerInvoke
arg types and ranges, cooldown, player data may not be loaded yet
PlayerAdded
load inside
pcall
, not-loaded flag, table entry cleared in
PlayerRemoving
CharacterAdded
every connection, loop, and track has a
CharacterRemoving
or
Died
cleanup
LoadAnimation
called on an Animator, loaded once per character, has a matching
Stop
GetAsync
,
SetAsync
pcall
, retry,
BindToClose
,
UpdateAsync
for merges
wait(
,
spawn(
,
delay(
,
while true
task.*
, an event instead of polling, an exit condition
game.Players
,
game.Workspace
, any
game.X
game:GetService
cached at the top
通读文件一遍。下面的每个 token 都会触发一组固定检查;每项失败的检查都要报告,即使同一行已经有更严重的问题。
看到检查
Touched
防抖;
hit
可能是 Accessory 的 handle;部件移除时清理连接
OnServerEvent
,
OnServerInvoke
参数类型和范围、冷却机制、玩家数据可能尚未加载
PlayerAdded
在
pcall
中加载、设置未加载标记、在
PlayerRemoving
中清除表项
CharacterAdded
每个连接、循环和轨道都要在
CharacterRemoving
或
Died
时清理
LoadAnimation
在 Animator 上调用;每个角色只加载一次;具有对应的
Stop
GetAsync
,
SetAsync
pcall
、重试、
BindToClose
,合并时使用
UpdateAsync
wait(
,
spawn(
,
delay(
,
while true
改用
task.*
、用事件代替轮询、设置退出条件
game.Players
,
game.Workspace
, 任意
game.X
在顶部缓存
game:GetService

1. Client/server mistakes

1. 客户端/服务器错误

SymptomCauseFix
Exploiters give themselves items or coinsServer reads an amount, id, or position from
OnServerEvent
args
Server computes the value itself; client sends intent only (
"buyItem", itemId
)
Remote handler errors on odd inputNo type or range check on args
if typeof(amount) ~= "number" or amount < 1 or amount > MAX then return end
Remote spam lags the serverNo cooldownTrack
lastFire[player] = os.clock()
; drop calls inside the cooldown
Script runs nowhereLocalScript in ServerScriptService, or Script in StarterPlayerScriptsLocalScripts: StarterPlayerScripts, StarterCharacterScripts, StarterGui, StarterPack. Scripts: ServerScriptService, Workspace. ModuleScripts: ReplicatedStorage (shared) or ServerStorage (server only)
Client change doesn't show for othersClient edits Workspace directlyClient fires a RemoteEvent; server makes the change
Remote is nil on the clientCreated by a Script after the LocalScript looked for itPut RemoteEvents in ReplicatedStorage in the Explorer, not
Instance.new
at runtime, and
WaitForChild
on the client
症状原因修复
作弊者给自己发放物品或金币服务器从
OnServerEvent
参数中读取数量、ID 或位置
由服务器自行计算数值;客户端只发送意图(
"buyItem", itemId
)
Remote 处理程序在遇到异常输入时出错参数没有类型或范围检查
if typeof(amount) ~= "number" or amount < 1 or amount > MAX then return end
Remote 洪泛导致服务器卡顿没有冷却机制跟踪
lastFire[player] = os.clock()
;丢弃冷却期内的调用
脚本完全不运行LocalScript 位于 ServerScriptService,或 Script 位于 StarterPlayerScriptsLocalScript:StarterPlayerScripts、StarterCharacterScripts、StarterGui、StarterPack。Script:ServerScriptService、Workspace。ModuleScript:ReplicatedStorage(共享)或 ServerStorage(仅服务器)
客户端的更改对其他人不可见客户端直接修改 Workspace客户端触发 RemoteEvent;服务器执行更改
客户端上的 Remote 为 nilScript 在 LocalScript 查找之后才创建它在 Explorer 中将 RemoteEvent 放入 ReplicatedStorage,而不是在运行时使用
Instance.new
创建,并在客户端使用
WaitForChild

2. Bugs that crash on respawn or leave

2. 在角色重生或玩家离开时崩溃的错误

SymptomCauseFix
attempt to index nil
on
playerData[player]
Remote or Touched fires before the async load in PlayerAdded finishesGuard:
local data = playerData[player]; if not data then return end
attempt to perform arithmetic on nil
after leave
PlayerRemoving
reads a table entry that never loaded
Same guard; never save when the load failed
Works in Play Solo, breaks in Team Test or liveScript assumes the character exists at start
player.Character or player.CharacterAdded:Wait()
Touched gives 2 to 5 pickups per coin
Touched
fires once per limb
Debounce:
if coin:GetAttribute("Taken") then return end; coin:SetAttribute("Taken", true)
before awarding
Pickup ignores hats and tools
hit.Parent
is the Accessory, not the character
local char = hit:FindFirstAncestorOfClass("Model")
then
Players:GetPlayerFromCharacter(char)
Workspace.Folder.Part is not a valid member
Dot chain runs before the part streams or loads
WaitForChild
on the client;
FindFirstChild
plus a nil check on the server
Script errors once then goes silentError thrown inside a connection handler kills only that call, but an error at top level stops the whole scriptMove setup into functions; wrap risky calls in
pcall
症状原因修复
在
playerData[player]
上出现
attempt to index nil
Remote 或 Touched 在 PlayerAdded 中的异步加载完成前触发添加保护:
local data = playerData[player]; if not data then return end
玩家离开后出现
attempt to perform arithmetic on nil
PlayerRemoving
读取了从未加载的表项
使用同样的保护;加载失败时绝不保存
在 Play Solo 中正常,在 Team Test 或正式环境中失效脚本假设角色一开始就存在
player.Character or player.CharacterAdded:Wait()
每个金币触发 2 到 5 次拾取
Touched
会为每条肢体触发一次
防抖:在发放奖励前执行
if coin:GetAttribute("Taken") then return end; coin:SetAttribute("Taken", true)
拾取时忽略帽子和工具
hit.Parent
是 Accessory,而不是角色
local char = hit:FindFirstAncestorOfClass("Model")
,然后使用
Players:GetPlayerFromCharacter(char)
Workspace.Folder.Part is not a valid member
点号链在部件流入或加载之前执行客户端使用
WaitForChild
;服务器使用
FindFirstChild
并检查 nil
脚本报错一次后便不再执行连接处理程序内部抛出的错误只会终止该次调用,但顶层错误会停止整个脚本将初始化逻辑移入函数;用
pcall
包裹有风险的调用

3. Memory leaks

3. 内存泄漏

SymptomCauseFix
Server memory climbs every respawn
CharacterAdded
connects Heartbeat, Touched, or a
while true
loop and never stops it
Store the connection; disconnect in
CharacterRemoving
or
humanoid.Died
. Loops:
while character.Parent do
Memory climbs per player who joinedTable keyed by
Player
never cleared
playerData[player] = nil
in
PlayerRemoving
Parts pile up invisibly
part.Parent = nil
instead of
part:Destroy()
Destroy()
disconnects events and frees the instance
Tweens or tracks keep running on destroyed objectsTrack or tween created per spawn, never stoppedStop and destroy in
CharacterRemoving
, or keep one per character
Sounds and particles stack
Instance.new
every event, never cleaned
Reuse one instance, or
Debris:AddItem(inst, seconds)
Rule of thumb: every
Connect
,
task.spawn
,
while
,
Instance.new
, and
LoadAnimation
inside a
PlayerAdded
or
CharacterAdded
handler needs a matching cleanup in
PlayerRemoving
or
CharacterRemoving
.
症状原因修复
服务器内存在每次角色重生时持续增长
CharacterAdded
连接 Heartbeat、Touched 或
while true
循环,却从未停止
存储连接;在
CharacterRemoving
或
humanoid.Died
时断开连接。循环:
while character.Parent do
内存随着每位加入的玩家不断增长以 Player 为键的表从未清除在
PlayerRemoving
中执行
playerData[player] = nil
部件在不可见的情况下不断堆积使用了
part.Parent = nil
,而不是
part:Destroy()
Destroy()
会断开事件并释放实例
Tween
或
Track
在已销毁的对象上持续运行
每次生成都会创建 Track 或 Tween,却从未停止在
CharacterRemoving
中停止并销毁,或者每个角色只保留一个
声音和粒子不断叠加每次事件都使用
Instance.new
,且从未清理
复用一个实例,或使用
Debris:AddItem(inst, seconds)
经验法则:每个位于
PlayerAdded
或
CharacterAdded
处理程序中的
Connect
、
task.spawn
、
while
、
Instance.new
和
LoadAnimation
,都需要在
PlayerRemoving
或
CharacterRemoving
中有对应的清理。

4. Performance

4. 性能

SymptomCauseFix
Everything slows under load
wait()
,
spawn()
,
delay()
task.wait()
,
task.spawn()
,
task.delay()
Script Performance window shows one script at the top
FindFirstChild
,
GetChildren
,
GetDescendants
, or
game:GetService
inside Heartbeat or a tight loop
Cache references once above the loop
Polling loop per player
while true do task.wait(0.1) if humanoid.MoveDirection ...
Use the event:
humanoid.Running
,
humanoid.StateChanged
,
part:GetPropertyChangedSignal("Value")
Heartbeat handler does work every frameNo throttleAccumulate
dt
and run every 0.2 to 0.5s
String building lags
s = s .. x
in a loop
Collect into a table,
table.concat
Physics lag with static buildsUnanchored decorative parts
Anchored = true
on anything that never moves
Low FPS on big mapsStreamingEnabled offTurn it on in Workspace properties; guard client code for parts being nil
Unknown causeGuessingCtrl+F6 MicroProfiler, Ctrl+P to pause a frame, find the wide bar. Wrap suspects in
debug.profilebegin("Name")
/
debug.profileend()
症状原因修复
负载较高时整体变慢使用
wait()
、
spawn()
、
delay()
改用
task.wait()
、
task.spawn()
、
task.delay()
Script Performance 窗口显示某个脚本耗时最高在 Heartbeat 或紧凑循环中调用
FindFirstChild
、
GetChildren
、
GetDescendants
或
game:GetService
在循环上方缓存引用一次
每位玩家都有一个轮询循环
while true do task.wait(0.1) if humanoid.MoveDirection ...
使用事件:
humanoid.Running
、
humanoid.StateChanged
、
part:GetPropertyChangedSignal("Value")
Heartbeat 处理程序每帧都执行工作没有节流累积
dt
,每 0.2 到 0.5 秒执行一次
构建字符串时出现卡顿在循环中执行
s = s .. x
收集到表中,再使用
table.concat
静态建筑产生物理卡顿装饰部件未锚定对所有永不移动的部件设置
Anchored = true
大型地图的 FPS 较低关闭了 StreamingEnabled在 Workspace 属性中开启;客户端代码要防护部件为 nil
原因不明依靠猜测使用 Ctrl+F6 MicroProfiler,Ctrl+P 暂停一帧,并找到较宽的条形。将可疑代码包裹在
debug.profilebegin("Name")
/
debug.profileend()
中

5. Studio-specific anti-patterns

5. Studio 特有的反模式

  • game.Players
    ,
    game.ReplicatedStorage
    as dot access. Works, but
    game:GetService("Players")
    is the documented path and survives renames. Cache it once at the top of the script.
  • Instance.new("Part", workspace)
    . The parent argument replicates every later property change one at a time. Set properties first,
    Parent
    last.
  • A script inside a Workspace part that assumes
    script.Parent
    is ready. It is, but duplicated parts run duplicated scripts. Prefer one script in ServerScriptService that loops over a folder or a CollectionService tag.
  • Logic in a ScreenGui with
    ResetOnSpawn = true
    . The LocalScript restarts on every respawn and reconnects everything. Set
    ResetOnSpawn = false
    unless the UI must reset.
  • while wait() do
    . Yields the whole script; use an event or
    task.wait
    .
  • print
    in Heartbeat. The Output window itself becomes the bottleneck.
  • Testing DataStores in Play mode without Game Settings > Security > Enable Studio Access to API Services. Calls error with 403 and nothing saves.
  • 将
    game.Players
    、
    game.ReplicatedStorage
    作为点号访问。这样虽然可行,但
    game:GetService("Players")
    才是文档推荐的路径,并且重命名后仍然有效。应在脚本顶部缓存一次。
  • Instance.new("Part", workspace)
    。将 parent 作为参数传入,会导致之后的每次属性变更逐个复制。先设置属性,最后设置
    Parent
    。
  • 放在 Workspace 部件内部并假设
    script.Parent
    已就绪的脚本。它确实已就绪,但复制的部件会运行重复的脚本。更好的做法是在 ServerScriptService 中放置一个脚本,遍历文件夹或 CollectionService 标签。
  • ScreenGui 中使用
    ResetOnSpawn = true
    的逻辑。LocalScript 会在每次角色重生时重启,并重新连接所有内容。除非 UI 必须重置,否则将
    ResetOnSpawn
    设为
    false
    。
  • while wait() do
    。它会让整个脚本产生 yield;应使用事件或
    task.wait
    。
  • 在 Heartbeat 中使用
    print
    。Output 窗口本身会成为瓶颈。
  • 在未启用 Game Settings > Security > Enable Studio Access to API Services 的情况下于 Play 模式测试 DataStore。调用会报 403 错误,并且不会保存任何内容。

6. Animation scripts

6. 动画脚本

SymptomCauseFix
Deprecation warning, animation may not replicate
humanoid:LoadAnimation(anim)
humanoid:WaitForChild("Animator"):LoadAnimation(anim)
Animation stacks or stutters
LoadAnimation
called every time the action fires
Load once per character, store the track, reuse it
Animation never stopsOnly
Play
is called
track:Stop()
on the opposite event (
Running
speed 0, tool unequipped,
Died
)
Walk overrides the attackWrong
Priority
track.Priority = Enum.AnimationPriority.Action
Animation plays once and freezes
Looped
unset for an idle
track.Looped = true
for idles; leave false for one-shots
Works for you, invisible for othersAnimation asset not owned by the game owner or groupRe-upload under the account or group that owns the place
Lag before the animation showsPlayer animations loaded on the serverLoad and play player-character animations in a LocalScript; the engine replicates them
Tracks pile upNever destroyed
track:Destroy()
in
CharacterRemoving
, or rely on one-per-character reuse
症状原因修复
出现弃用警告,动画可能无法复制
humanoid:LoadAnimation(anim)
humanoid:WaitForChild("Animator"):LoadAnimation(anim)
动画叠加或卡顿每次动作触发时都调用
LoadAnimation
每个角色只加载一次,存储轨道并重复使用
动画永不停止只调用了
Play
在相反事件发生时调用
track:Stop()
(
Running
速度为 0、工具卸下、
Died
)
行走动画覆盖攻击动画
Priority
设置错误
track.Priority = Enum.AnimationPriority.Action
动画播放一次后冻结闲置动画未设置
Looped
闲置动画设置
track.Looped = true
;一次性动画保持 false
自己能看到,其他人看不到动画资产不属于游戏所有者或群组在拥有该 place 的账户或群组下重新上传
动画显示前出现卡顿玩家动画在服务器上加载在 LocalScript 中加载并播放玩家角色动画;引擎会负责复制
轨道不断堆积从未销毁在
CharacterRemoving
中执行
track:Destroy()
,或者每个角色只复用一个轨道

7. DataStore review

7. DataStore 审查

SymptomCauseFix
Script errors with "Request was throttled" or HTTP 5xxBare
GetAsync
or
SetAsync
Wrap every call in
pcall
; retry up to 3 times with
task.wait(2 ^ attempt)
Player data resets to defaultLoad failed, script fell back to defaults, then saved themIf the load pcall fails, mark the player as not loaded and skip the save
Coins lost on server shutdownOnly saving in
PlayerRemoving
Add
game:BindToClose(function() for each player: save end)
; in Studio this also runs when you stop Play
Two servers overwrite each other
GetAsync
then
SetAsync
UpdateAsync(key, function(old) return new end)
for anything that merges
"DataStore request was added to queue" spamSaving on every changeSave on
PlayerRemoving
,
BindToClose
, and at most every 60 to 120s
Old data unreadable after a schema changeRaw
UserId
as the key
String key with a version:
"v1_" .. player.UserId
Nothing saves in StudioAPI access offGame Settings > Security > Enable Studio Access to API Services
Table saved as
nil
Value contains an Instance, a function, or a mixed-key tableSave plain numbers, strings, booleans, and array or string-key tables only
症状原因修复
脚本出现 "Request was throttled" 或 HTTP 5xx 错误直接调用
GetAsync
或
SetAsync
将每次调用包裹在
pcall
中;最多重试 3 次,并使用
task.wait(2 ^ attempt)
玩家数据重置为默认值加载失败,脚本回退到默认值,随后又将这些默认值保存如果加载
pcall
失败,将玩家标记为未加载,并跳过保存
服务器关闭时丢失金币只在
PlayerRemoving
中保存
添加
game:BindToClose(function() for each player: save end)
;在 Studio 中,停止 Play 时也会执行此函数
两台服务器相互覆盖数据先执行
GetAsync
,再执行
SetAsync
任何需要合并的操作都使用
UpdateAsync(key, function(old) return new end)
反复出现 "DataStore request was added to queue"每次数据变化都保存在
PlayerRemoving
、
BindToClose
时保存,并且最多每 60 到 120 秒保存一次
更改数据结构后旧数据无法读取直接使用
UserId
作为键
使用带版本号的字符串键:
"v1_" .. player.UserId
在 Studio 中没有保存任何内容未启用 API 访问Game Settings > Security > Enable Studio Access to API Services
表被保存为
nil
值包含 Instance、函数或混合键表只能保存普通数字、字符串、布尔值,以及数组或字符串键表

Common mistakes in the review itself

审查本身的常见错误

  • Flagging
    --!strict
    , type annotations, or file structure. Studio devs cannot act on these without changing their workflow.
  • Suggesting a framework (Knit, ProfileService) as the first fix. Name the direct fix; mention a library only if the dev asks.
  • Listing twenty items with no order. Severity first, one fix each.
  • 将
    --!strict
    、类型注解或文件结构列为问题。Studio 开发者如果不改变工作流,就无法处理这些问题。
  • 将框架(Knit、ProfileService)作为首要修复方案。应指出直接修复方法;只有开发者提出要求时才提及库。
  • 列出二十个问题却不排序。应先列严重问题,每项只给一个修复方案。