ワンクリックで
roblox-runtime
StreamingEnabled, performance optimization, memory management, object pooling, mobile targets.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
StreamingEnabled, performance optimization, memory management, object pooling, mobile targets.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Service hierarchy, 7 foundational patterns, cross-platform input. Client-server architecture, module patterns, framework options.
Luau language fundamentals, type system, OOP, deprecation table, error patterns.
Roblox AnalyticsService: custom events, economy tracking, funnels, rate limits, event taxonomy.
Animations, particles, tweens, ContentProvider, visual effects.
Code review with security, performance, and monetization lenses for Roblox projects
DataStores, ProfileStore, session locking, data persistence patterns.
| name | roblox-runtime |
| description | StreamingEnabled, performance optimization, memory management, object pooling, mobile targets. |
| last_reviewed | "2026-05-22T00:00:00.000Z" |
Load this reference when:
Performance optimization is not a one-time task. It should be revisited after every significant content addition and tested across the full range of target devices.
Load Full Reference below only when you need specific optimization techniques or benchmarks.
Key rules:
..| Metric | Desktop | Mobile |
|---|---|---|
| Frame Rate | 60 fps | 30 fps minimum |
| Memory Budget | ~1 GB | ~500 MB |
| Network | Minimize remote frequency | Same, with smaller payloads |
| Load Time | Under 10 seconds | Under 15 seconds |
Key principles:
UnionOperation recalculates collision geometry at runtime and is more expensive.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 client load and never stream.
When instances stream out, they are parented to nil - not destroyed. Luau state persists if they stream back in. Removal signals fire, but local-only property changes may be lost.
StreamingTargetRadius - radius (studs) engine keeps loaded. Start at 256, tune.StreamingMinRadius - guaranteed radius. Set ~64 for nearby content.StreamingPauseMode - what happens during load (Default, Disabled, ClientPhysicsPause).ModelStreamingMode - per-model: Atomic (all descendants load together), Persistent (never streams out), PersistentPerPlayer, Nonatomic.WaitForChild() on client for any Workspace instance. Never use workspace.MyPart dot access in LocalScripts - the instance may not be loaded yet.WaitForChild("Name", 30). Without timeout, thread hangs forever if instance never streams in.math.huge as timeout. The instance may never stream in.ModelStreamingMode = Atomic when all parts must appear together.Player:RequestStreamAroundAsync(location) to pre-fetch areas before teleporting.The official recommended pattern for streaming-aware code:
local CollectionService = game:GetService("CollectionService")
local tag = "Interactive"
local active: {[Instance]: any} = {}
CollectionService:GetInstanceAddedSignal(tag):Connect(function(instance)
active[instance] = true
-- Set up connections, UI, effects for this instance
end)
CollectionService:GetInstanceRemovedSignal(tag):Connect(function(instance)
active[instance] = nil
-- Cleanup: disconnect events, remove UI, stop effects
end)
-- Handle instances already present when script starts
for _, instance in CollectionService:GetTagged(tag) do
active[instance] = true
end
This handles stream-in and stream-out automatically. No WaitForChild needed.
For client-side observation of streaming instances with automatic cleanup:
local Streamable = require(ReplicatedStorage.Modules.Streamable)
local model = workspace:WaitForChild("MyModel")
local partStreamable = Streamable.new(model, "SomePart")
partStreamable:Observe(function(part, trove)
-- Called when part streams in
-- trove handles cleanup automatically when part streams out
trove:Add(function()
-- Cleanup code
end)
end)
-- Check existence directly
if partStreamable.Instance then
-- part is currently loaded
end
partStreamable:Destroy() -- clean up when done
-- Server: pre-fetch area before teleporting player
Player:RequestStreamAroundAsync(targetLocation)
-- Keep an area permanently loaded for a player
Player:AddReplicationFocus(importantPart)
-- Remove when no longer needed
Player:RemoveReplicationFocus(importantPart)
Source: Roblox Instance Streaming docs, Sleitnick/RbxUtil Streamable (MIT)
BasePart.Anchored = true for terrain decorations, buildings, props, and anything that should not move.Never scatter RunService.Heartbeat:Connect(...) across dozens of scripts. Consolidate into a single manager.
-- HeartbeatManager (single Script in ServerScriptService or a ModuleScript)
local RunService = game:GetService("RunService")
local HeartbeatManager = {}
HeartbeatManager._callbacks = {} :: { [string]: (dt: number) -> () }
function HeartbeatManager:Register(id: string, callback: (dt: number) -> ())
self._callbacks[id] = callback
end
function HeartbeatManager:Unregister(id: string)
self._callbacks[id] = nil
end
RunService.Heartbeat:Connect(function(dt: number)
for _, callback in self._callbacks do
callback(dt)
end
end)
return HeartbeatManager
Usage from other modules:
local HeartbeatManager = require(path.to.HeartbeatManager)
HeartbeatManager:Register("EnemyAI", function(dt: number)
-- update all enemies
end)
-- When no longer needed:
HeartbeatManager:Unregister("EnemyAI")
-- Pre-allocate a table with 100 slots
local results = table.create(100)
for i = 1, 100 do
results[i] = computeValue(i)
end
-- BAD: creates a new string object every iteration
local result = ""
for i = 1, 1000 do
result = result .. tostring(i) .. ","
end
-- GOOD: build a table, join once
local parts = table.create(1000)
for i = 1, 1000 do
parts[i] = tostring(i)
end
local result = table.concat(parts, ",")
Every :Connect() call returns a RBXScriptConnection. Store it and disconnect when done.
-- Event Cleanup Pattern
local Cleaner = {}
Cleaner.__index = Cleaner
function Cleaner.new()
local self = setmetatable({}, Cleaner)
self._connections = {} :: { RBXScriptConnection }
self._instances = {} :: { Instance }
return self
end
function Cleaner:Add(connection: RBXScriptConnection)
table.insert(self._connections, connection)
return connection
end
function Cleaner:AddInstance(instance: Instance)
table.insert(self._instances, instance)
return instance
end
function Cleaner:Clean()
for _, conn in self._connections do
if conn.Connected then
conn:Disconnect()
end
end
table.clear(self._connections)
for _, inst in self._instances do
inst:Destroy()
end
table.clear(self._instances)
end
return Cleaner
Usage:
local Cleaner = require(path.to.Cleaner)
local cleaner = Cleaner.new()
cleaner:Add(workspace.ChildAdded:Connect(function(child)
print(child.Name, "added")
end))
cleaner:Add(Players.PlayerRemoving:Connect(function(player)
print(player.Name, "left")
end))
-- When this system shuts down or the player leaves:
cleaner:Clean()
:Destroy() rather than setting Parent = nil. :Destroy() locks the instance, disconnects all events on it, and marks it for garbage collection.Parent = nil keeps the instance alive if anything still references it.-- BAD: mutual references prevent garbage collection
local a = {}
local b = {}
a.ref = b
b.ref = a
-- Neither a nor b can be collected until both references are broken
Break references explicitly when done: a.ref = nil; b.ref = nil.
Use Instance.Destroying to run cleanup when an instance is about to be destroyed:
local part = Instance.new("Part")
part.Destroying:Connect(function()
-- clean up related data, disconnect connections, etc.
end)
For timed cleanup of temporary instances (projectiles, effects):
local Debris = game:GetService("Debris")
local bullet = Instance.new("Part")
bullet.Parent = workspace
Debris:AddItem(bullet, 5) -- destroyed after 5 seconds
-- BAD: three separate remote calls
remoteHealth:FireClient(player, health)
remoteAmmo:FireClient(player, ammo)
remoteStamina:FireClient(player, stamina)
-- GOOD: one call with a table
remotePlayerState:FireClient(player, {
health = health,
ammo = ammo,
stamina = stamina,
})
For high-frequency, non-critical data such as position or rotation updates, use UnreliableRemoteEvent. Dropped packets are acceptable because the next update will correct the state.
-- In ReplicatedStorage, create an UnreliableRemoteEvent named "PositionSync"
local posSync = ReplicatedStorage:WaitForChild("PositionSync")
-- Server: fire frequently without guaranteeing delivery
RunService.Heartbeat:Connect(function()
for _, player in Players:GetPlayers() do
posSync:FireClient(player, npcPositions)
end
end)
hp instead of hitPoints).Create multiple versions of a model at different detail levels and swap based on distance:
local function setLOD(model: Model, playerPosition: Vector3)
local distance = (model:GetPivot().Position - playerPosition).Magnitude
if distance < 100 then
-- show high-detail version
elseif distance < 300 then
-- show medium-detail version
else
-- show low-detail version or hide
end
end
Roblox also has built-in MeshPart.RenderFidelity (Automatic, Performance, Precise) which controls mesh LOD.
BasePart.CastShadow = false on distant or small parts.SurfaceLight, PointLight, SpotLight on distant objects.| Property | Recommended Max |
|---|---|
Particles per emitter (Rate) | ~200 |
| Total active emitters in view | ~20 |
Beam segments (Segments) | 10-20 |
Trail MaxLength | Keep short for mobile |
ParticleEmitter.Enabled = false when off-screen or far away.| Use Case | Max Resolution |
|---|---|
| General props, walls, floors | 512x512 |
| Hero assets (player characters, key items) | 1024x1024 |
| UI icons, decals | 256x256 to 512x512 |
| Sky/environment | 1024x1024 |
Decal over Texture when the surface only needs one face covered. Decals are simpler to render.UserInputService:GetPlatform() or screen size to detect mobile and reduce detail.Rate of particle emitters on mobile.Lifetime to keep fewer active particles.GuiObject.Active = true to ensure touch events register.if UserInputService.TouchEnabled then
workspace.StreamingTargetRadius = 128 -- lower than desktop
workspace.StreamingMinRadius = 48
end
Stats():GetTotalMemoryUsageMb() and warn/act if approaching 500 MB.The MicroProfiler displays a real-time flame graph of what the engine is doing each frame.
How to read it:
Ctrl+F6 to open. Press Ctrl+P to pause and inspect a frame.Heartbeat - your Heartbeat scripts. If wide, your per-frame logic is too heavy.Physics - collision and simulation. Reduce unanchored parts.Render/Perform - GPU-bound. Reduce draw calls, textures, particles.Replication - network overhead. Reduce remote calls and replicated property changes.microprofiler dump (Ctrl+F6 > Dump) to save a .html file for offline analysis.F9 in-game or in Studio to open.local Stats = game:GetService("Stats")
-- Total memory in MB
local totalMemory = Stats:GetTotalMemoryUsageMb()
-- Specific categories
local instanceMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.Instances)
local scriptMemory = Stats:GetMemoryUsageMbForTag(Enum.DeveloperMemoryTag.LuaHeap)
print(string.format("Total: %.1f MB | Instances: %.1f MB | Lua: %.1f MB",
totalMemory, instanceMemory, scriptMemory))
Never guess where the bottleneck is. Use the MicroProfiler and memory stats to find the actual hot path before changing code.
Focus effort on code that runs every frame (Heartbeat, RenderStepped) or on every player action. Code that runs once at startup is rarely worth optimizing.
-- BAD: loop over every part in workspace
for _, part in workspace:GetDescendants() do
if (part.Position - origin).Magnitude < 50 then
-- ...
end
end
-- GOOD: spatial query
local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Include
params.FilterDescendantsInstances = { workspace.Enemies }
local parts = workspace:GetPartBoundsInBox(
CFrame.new(origin),
Vector3.new(100, 100, 100), -- 50-stud radius box
params
)
Reuse instances instead of creating and destroying them repeatedly.
-- Object Pool Pattern
local ObjectPool = {}
ObjectPool.__index = ObjectPool
function ObjectPool.new(template: Instance, initialSize: number)
local self = setmetatable({}, ObjectPool)
self._template = template
self._available = table.create(initialSize)
self._active = {} :: { [Instance]: boolean }
-- Pre-populate
for i = 1, initialSize do
local clone = template:Clone()
clone.Parent = nil
table.insert(self._available, clone)
end
return self
end
function ObjectPool:Get(): Instance
local obj: Instance
if #self._available > 0 then
obj = table.remove(self._available)
else
obj = self._template:Clone()
end
self._active[obj] = true
return obj
end
function ObjectPool:Return(obj: Instance)
if not self._active[obj] then
return
end
self._active[obj] = nil
obj.Parent = nil
-- Reset state as needed (position, visibility, etc.)
if obj:IsA("BasePart") then
obj.CFrame = CFrame.new(0, -1000, 0) -- move off-screen
obj.Anchored = true
obj.Velocity = Vector3.zero
end
table.insert(self._available, obj)
end
function ObjectPool:ReturnAll()
for obj in self._active do
self:Return(obj)
end
end
function ObjectPool:Destroy()
for obj in self._active do
obj:Destroy()
end
for _, obj in self._available do
obj:Destroy()
end
table.clear(self._active)
table.clear(self._available)
end
return ObjectPool
Usage:
local ObjectPool = require(path.to.ObjectPool)
local bulletTemplate = ReplicatedStorage.Assets.Bullet
local bulletPool = ObjectPool.new(bulletTemplate, 50)
-- Spawn a bullet
local bullet = bulletPool:Get()
bullet.CFrame = firePoint
bullet.Parent = workspace
-- Return when done
bulletPool:Return(bullet)
Do not load all assets at game start. Use StreamingEnabled or manually load content as the player approaches.
local function onPlayerMoved(position: Vector3)
for _, zone in zones do
local distance = (zone.center - position).Magnitude
if distance < zone.loadRadius and not zone.loaded then
zone:Load()
elseif distance > zone.unloadRadius and zone.loaded then
zone:Unload()
end
end
end
Optimizing code that is not a bottleneck wastes development time and often makes code harder to read. Always profile first.
-- ANTI-PATTERN: creating parts every frame
RunService.Heartbeat:Connect(function()
local part = Instance.new("Part") -- allocation every frame
part.Parent = workspace
task.delay(1, function()
part:Destroy()
end)
end)
-- FIX: use an object pool (see section 10)
-- ANTI-PATTERN: sending entire inventory every update
remote:FireClient(player, fullInventoryTable) -- could be thousands of entries
-- FIX: send only changes
remote:FireClient(player, { added = { itemId }, removed = { oldItemId } })
A game that runs at 60 fps on a gaming PC may run at 10 fps on a phone. Always test on actual mobile hardware, not just the Studio emulator.
Common leak sources:
Detect leaks by monitoring Stats:GetTotalMemoryUsageMb() over time. If memory grows continuously during gameplay without stabilizing, there is a leak.
-- Simple memory monitor
local lastMemory = 0
task.spawn(function()
while true do
local current = game:GetService("Stats"):GetTotalMemoryUsageMb()
local delta = current - lastMemory
if delta > 10 then
warn(string.format("Memory spike: +%.1f MB (total: %.1f MB)", delta, current))
end
lastMemory = current
task.wait(10)
end
end)