MicroProfiler: Stop Guessing, Start Measuring
The MicroProfiler is Roblox's built-in profiling tool and the only reliable way to diagnose performance issues. Press Ctrl+F6 in a running game to open it. It displays a timeline of every frame showing exactly what the engine spent time on. The top bar shows the server, and the bottom shows the client. Look for bars that exceed the 16.67ms frame budget (60fps target). Common labels you will see: Render (drawing the scene, GPU-bound if dominant), Physics (collision and simulation), Heartbeat (your scripts running per frame), and Replication (network syncing). If Render dominates, you have too many visible instances or overly complex meshes. If Heartbeat dominates, your scripts are doing too much per frame. If Replication dominates, you are sending too much data between client and server. Always profile in a live server with realistic player counts because Studio testing hides most performance issues.
Part Count and Instance Reduction
The single biggest performance factor in most Roblox games is how many instances exist in the workspace. Every Part, MeshPart, and Model the engine must consider for rendering, physics, and streaming has a cost. Games with over 100,000 parts will lag on most hardware. Reduction strategies: merge decorative unions using Union operations to collapse multiple parts into one, convert detailed models to single MeshParts exported from Blender, set CanCollide and CanQuery to false on decorative parts that do not need physics, and use Anchored = true on everything that does not need to move. For trees, grass, and repeated decorations, use a single MeshPart with a texture atlas instead of dozens of small parts arranged into a shape. Enable the Instance Count stats display in Studio (View > Stats) to track your total instance count. Aim for under 50,000 instances in the actively rendered area.
- Union decorative Part clusters into single UnionOperations
- Convert complex models to single MeshParts via Blender export
- Set CanCollide and CanQuery to false on non-interactive decorations
- Anchor all static geometry (Anchored = true)
- Target under 50,000 instances in the actively streamed area
StreamingEnabled: The Most Impactful Single Setting
StreamingEnabled is the single most impactful optimization for any game with a map larger than a single room. It dynamically loads and unloads parts of the map based on player proximity, drastically reducing the number of instances the client must render and store in memory. Enable it in Workspace properties. Set StreamingTargetRadius to the minimum distance that maintains gameplay quality (256 studs is a good starting point for most games). StreamingMinRadius defines the guaranteed loaded area around the player. Set StreamingIntegrity to PauseOutsideLoadedArea to prevent players from falling through unloaded terrain, or Default for seamless streaming that occasionally shows unloaded areas. Critical gameplay objects (spawn points, player inventory UIs, global scripts) must be placed in containers that are not affected by streaming: ReplicatedStorage, ReplicatedFirst, or use the Persistent property on essential models. Test streaming by teleporting across your map rapidly and checking for pop-in or unloaded areas.
Script Optimization and Memory Leaks
Script performance issues fall into two categories: per-frame cost and memory leaks. For per-frame cost, audit every RenderStepped and Heartbeat connection. Each callback adds to the frame budget. Avoid running expensive operations (raycasts, table iterations over large datasets, Instance:FindFirstChild chains) every frame. Use debouncing, event-driven logic, and caching. For memory leaks, the most common cause is connections that are never disconnected. Every time you call event:Connect(), store the connection object and call :Disconnect() when the listener is no longer needed. Destroyed instances do not automatically disconnect their event connections in all cases. Other leak sources: tables that grow indefinitely without cleanup, cloned instances that are never parented (they persist in memory), and animations loaded but never garbage collected. Use the Memory tab in Studio's Developer Console to track memory usage over time. If it grows continuously without stabilizing, you have a leak.
Network Optimization: RemoteEvent Spam
Every RemoteEvent:FireServer() and FireClient() call consumes bandwidth and server processing time. Firing remotes every frame (such as sending mouse position every RenderStepped) will devastate server performance with 20+ players. Throttle remote calls to the minimum frequency needed: mouse position updates 10 times per second instead of 60, damage numbers batched into a single remote per frame instead of one per hit, and UI updates sent only when values change rather than on a timer. On the server, validate and rate-limit all incoming remotes. If a player sends more than a reasonable number of calls per second, drop the excess rather than processing them. Use UnreliableRemoteEvents for non-critical data like cosmetic updates or position interpolation where occasional packet loss is acceptable. They have lower overhead than standard RemoteEvents.
Rendering Optimization: LOD and Draw Distance
For rendering, the engine must draw every visible instance within the camera's range. Reduce render load by implementing level-of-detail (LOD) manually: swap high-poly MeshParts for low-poly versions at distance using magnitude checks on a timer. Set RenderFidelity on MeshParts to Performance for distant objects and Precise only for close-up hero assets. Reduce the shadow distance in Lighting properties because shadows are one of the most expensive rendering features. Disable CastShadow on small parts, interior objects, and anything the player is unlikely to see from a distance. For particle effects, set ParticleEmitter.Rate to the minimum that looks acceptable and reduce Lifetime so particles do not accumulate. A single ParticleEmitter with Rate 100 and Lifetime 5 means 500 particles rendered simultaneously, which adds up fast across many emitters.
