Performance in Roblox is a multi-dimensional problem: part count drives physics and rendering costs, script logic drives CPU cost, event connections drive memory cost, and remote call frequency drives network cost. The most important habit is profiling before optimizing — the MicroProfiler and F9 console will tell you exactly where time and memory are going, and guessing wrong wastes development time while leaving the real bottleneck untouched.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/brockmartin/roblox-game-skill/llms.txt
Use this file to discover all available pages before exploring further.
Performance Targets
| Metric | Desktop | Mobile |
|---|---|---|
| Frame rate | 60 fps | 30 fps minimum |
| Memory budget | ~1 GB | ~500 MB |
| Load time | Under 10 seconds | Under 15 seconds |
| Remote calls | Under 50/sec per client | Same, smaller payloads |
Part Count Best Practices
Limits
- Per model: aim for a maximum of ~500 parts.
- Total scene: keep the visible scene under 10,000 parts.
- Fewer parts means less physics simulation, less rendering overhead, and faster replication on join.
MeshParts Over Unions
UnionOperation recalculates collision geometry at runtime and is more expensive than MeshPart. If you built something with unions, export it to a MeshPart:
- Select the union in Studio.
- Right-click → Export Selection.
- Re-import the exported
.objas a MeshPart.
StreamingEnabled
For large maps, enableWorkspace.StreamingEnabled to stream world content in and out based on player proximity:
| Property | Recommended Value | Notes |
|---|---|---|
StreamingTargetRadius | 256 studs | Tune down for mobile |
StreamingMinRadius | 64 studs | Guaranteed nearby content |
StreamingPauseMode | Default | Pauses client physics when loading |
ModelStreamingMode = Persistent.
Anchor Every Static Part
Unanchored parts enter the physics solver even when motionless, consuming CPU every frame. SetBasePart.Anchored = true on all terrain decorations, buildings, props, and anything that should not move.
Script Optimization Patterns
Consolidated Heartbeat
ScatteringRunService.Heartbeat:Connect(...) across dozens of scripts wastes overhead with separate closures and callback dispatch. Consolidate into a single manager.
Cache Service References
Avoid FindFirstChild in Tight Loops
Table Pre-allocation
String Concatenation
task.wait() vs wait()
Always usetask.wait() instead of the deprecated global wait(). The legacy wait() has a minimum yield of ~0.03 seconds regardless of the argument passed, imposes internal throttling, and can delay tasks far longer than intended. task.wait() uses a precise scheduler with no minimum floor. The same applies to task.spawn() over spawn() and task.delay() over delay().
Memory Management
Disconnect Events and Destroy Instances
Every:Connect() call returns an RBXScriptConnection. If you never disconnect, the connection (and everything it captures in its closure) lives until the server restarts.
Always Use :Destroy()
Always call:Destroy() rather than setting Parent = nil. :Destroy() locks the instance, disconnects all events on it, and marks it for garbage collection. Setting Parent = nil keeps the instance alive if anything still references it.
Debris Service for Temporary Instances
Object Pooling
Reuse instances instead of creating and destroying them every time. CreatingInstance.new() in a Heartbeat loop is a common source of memory pressure and GC pauses.
Network Optimization
Batch Related Remotes
UnreliableRemoteEvent for High-Frequency Data
For position/rotation sync and other data where a dropped packet is harmless (the next update corrects state), useUnreliableRemoteEvent instead of RemoteEvent:
Delta Compression
Only send values that changed since the last update:Reduce Replicated Property Changes
Properties changed on the server replicate to all clients automatically. Set visual-only properties (particle colors, decoration tweens) on the client to avoid flooding replication bandwidth.Mobile Performance Considerations
- Part count: target 30–50% fewer parts than desktop. If your desktop budget is 10,000 parts, aim for 5,000–7,000 on mobile.
- Particle effects: halve
Rateon mobile; reduceLifetime; disable non-essential emitters entirely. - Touch-optimized UI: minimum touch target size is 44×44 points. Avoid hover-dependent UI — mobile has no hover state.
- Reduced streaming radius:
- Memory monitoring:
Profiling Tools
MicroProfiler (Ctrl+F6)
Real-time flame graph of what the engine does each frame. Press
Ctrl+P to pause on a frame. Wide bars are hot paths.Labels to watch:Heartbeat— your per-frame scriptsPhysics— unanchored partsRender/Perform— GPU loadReplication— network overhead
F9 Developer Console
- Memory tab: breakdown by Instances, Scripts, Signals, Sounds
- Stats tab: FPS, ping, data send/receive rates
- Log tab: errors and warnings with stack traces
Stats Service (Programmatic)
Spatial Queries Over Brute Force
Rendering Budgets
Particle Emitters
| Property | Recommended Max |
|---|---|
Rate per emitter | ~200 |
| Total active emitters in view | ~20 |
Beam Segments | 10–20 |
Trail MaxLength | Keep short for mobile |
ParticleEmitter.Enabled = false when the emitter is off-screen or far away.
Texture Resolution
| Use Case | Max Resolution |
|---|---|
| General props, walls, floors | 512×512 |
| Hero assets (characters, key items) | 1024×1024 |
| UI icons, decals | 256×256–512×512 |
| Sky/environment | 1024×1024 |
Decal over Texture for single-face coverage — decals are simpler to render.
Common Anti-Patterns
Creating/destroying instances in Heartbeat
Creating/destroying instances in Heartbeat
Sending full state over remotes instead of deltas
Sending full state over remotes instead of deltas
Not testing on mobile
Not testing on mobile
A game that runs at 60 fps on a gaming PC can easily drop below 15 fps on a mid-range phone. Always test on actual mobile hardware, not just the Studio emulator. Check for thermal throttling during extended play sessions on devices with 2–3 GB RAM.
Ignoring memory leaks
Ignoring memory leaks
Common leak sources:
- Event connections never disconnected — use the
Cleanerpattern above. - Instances removed from the hierarchy but still referenced in a module-level table.
- Closures capturing large upvalues that outlive their usefulness.
- Module-level tables (
playerData,cache, etc.) that grow indefinitely without cleanup onPlayerRemoving.
Stats:GetTotalMemoryUsageMb() climbs continuously during gameplay without stabilizing, there is a leak.Premature optimization
Premature optimization
Optimizing code that is not a bottleneck wastes development time and makes code harder to read. Always profile first with the MicroProfiler. Optimize only what the profiler proves is hot.