Mastering the MicroProfiler
The MicroProfiler (Ctrl+F6 in Studio) is the most powerful diagnostic tool available to Roblox developers. It shows you exactly where every millisecond of your frame time is going. The render bar shows GPU time, the physics bar shows Roblox physics simulation time, and the heartbeat bar shows your script execution time. Your target is 16.67ms total for 60 FPS. If your heartbeat bar consistently exceeds 8ms, your scripts are the bottleneck. If your render bar is the widest, you have too many draw calls or overdraw from particles and transparencies. Learn to pause the profiler with Ctrl+P and click individual bars to drill down into which specific functions are consuming time. This data-driven approach replaces guesswork with precision.
StreamingEnabled and Instance Management
StreamingEnabled is a workspace property that dynamically loads and unloads parts of your map based on player proximity. For large maps, this is essential. Without it, every player loads the entire world into memory on join, which destroys load times and memory usage on low-end devices. Configure StreamingMinRadius and StreamingTargetRadius to control how aggressively content streams in and out. Be aware that StreamingEnabled changes how client code accesses workspace objects. Parts outside the streaming radius do not exist on the client, so WaitForChild and StreamingEnabled-aware patterns become mandatory. Beyond streaming, manually manage instance lifetime. Destroy objects you no longer need, pool and reuse frequently created objects like projectiles and particles, and avoid parenting large model hierarchies in a single frame.
Preventing Memory Leaks
Memory leaks in Roblox most commonly come from three sources: event connections that are never disconnected, references to destroyed instances that prevent garbage collection, and tables that grow without bound. Every :Connect() call returns a connection object. If you connect events to objects that get destroyed (like player characters or temporary effects), you must disconnect those events or the callback function and everything it references stays in memory forever. Use the Maid or Trove cleanup pattern: store all connections in a cleanup object and call its destroy method when the parent object is removed. Monitor memory usage in the Developer Console (F9) under the Memory tab. A steadily climbing memory graph with no plateau means you have a leak.
- Always disconnect event connections when the associated object is destroyed
- Avoid storing references to Instances in module-level tables without cleanup logic
- Use weak tables (setmetatable({}, {__mode = "v"})) for caches that should not prevent garbage collection
- Profile with Developer Console Memory tab and watch for unbounded growth over 10+ minutes of gameplay
Task Scheduling and Script Performance
Expensive operations should never run on RenderStepped or Heartbeat every frame. If you need to update 500 NPCs, do not update all of them every heartbeat. Spread the work across multiple frames using a round-robin or priority queue. Update nearby NPCs every frame and distant ones every 5-10 frames. Replace busy-wait loops (while true do task.wait() end) with event-driven patterns wherever possible. Use task.defer for operations that do not need to happen this frame. Batch Instance property changes together rather than setting Position, then Size, then Color in separate statements across multiple frames, since each property change triggers internal updates. Cache frequently accessed properties in local variables instead of reading them from Instances repeatedly.
Render Optimization and Draw Calls
Every unique material and mesh combination in view generates a draw call. Hundreds of unique MeshParts with different textures will tank frame rates even if the polygon count is low. Reduce draw calls by reusing the same MeshId and TextureId across multiple parts, which lets the engine batch them. Minimize transparency because transparent objects cannot be batched and require sorting. Particle emitters are especially expensive on mobile: each active emitter adds overdraw proportional to its particle count and size. Set reasonable Rate and Lifetime values, and disable emitters that are off-screen. Use Level of Detail (LOD) by reducing model complexity for objects far from the camera, either through manual LOD swapping or Roblox's built-in RenderFidelity settings on MeshParts.
Network Budget and Replication
Every property change on a server-owned Instance is replicated to all clients, consuming network bandwidth. Roblox has a per-player network budget, and exceeding it causes replication lag where clients see delayed or stuttering updates. The biggest offenders are frequently updating part positions (like custom projectile systems), rapid property changes on many objects, and large RemoteEvent payloads. For custom physics objects, use UnreliableRemoteEvents for position updates that can tolerate occasional packet loss. Compress RemoteEvent data by sending only what changed rather than full state snapshots. For NPC movement, use server-side pathfinding but replicate only waypoints, letting clients interpolate positions locally to reduce bandwidth by 90% or more compared to replicating position every frame.
