Unity stuttering fix: find the cause first
The Glitchless team · October 27, 2026 · Unity 2022.3 LTS and Unity 6
A Unity game stutters in one of two ways. Jitter is movement or a camera that judders while the frame rate looks fine: a timing problem between physics and drawing. A hitch is one slow frame: garbage collection, a shader compiling, a scene loading or a burst of new objects. They have different fixes, so find out which one you have before you change anything.
The short answer
- Jittery movement and camera: turn on Rigidbody interpolation, move bodies in
FixedUpdate, and move the camera inLateUpdate - Garbage collection spikes: stop allocating every frame, and keep incremental GC on
- A hitch the first time something appears: warm up shader variants behind the loading screen
- Hitches when things spawn: pool objects with
UnityEngine.Pool.ObjectPoolinstead ofInstantiateandDestroy - A freeze on a level change: load scenes with
SceneManager.LoadSceneAsync()behind a loading screen
First, profile a build, not the Editor
The Unity Editor adds its own cost, so a game that stutters in Play mode can be smooth in a build. Make a Development Build with Autoconnect Profiler ticked, run it, and record in the Profiler window.
Click a spike in the CPU Usage module and switch to the Timeline view. GC.Collect in that frame is garbage collection. Shader.CreateGPUProgram the first time something appears is a shader compiling. Lots of Instantiate and Awake is a spawn burst. Even frames with juddering movement point to timing between physics and the camera.
In the Hierarchy view, sort by GC Alloc. Anything above 0 B every frame during normal play is garbage you'll pay for later. To log frame times from any build, add this to an object in your first scene:
FrameLog.cs
using System.Collections.Generic;
using UnityEngine;
// Logs the typical, 95th percentile and worst frame every 5 seconds, in any build.
public class FrameLog : MonoBehaviour
{
readonly List<float> frames = new List<float>(2048);
float total;
void Awake() => DontDestroyOnLoad(gameObject);
void Update()
{
float ms = Time.unscaledDeltaTime * 1000f;
frames.Add(ms);
total += ms;
if (total < 5000f) return;
frames.Sort();
int n = frames.Count;
Debug.Log($"frame ms: typical {frames[n / 2]:F1}, 95th {frames[(int)(n * 0.95f)]:F1}, worst {frames[n - 1]:F1}");
frames.Clear();
total = 0f;
}
}
Jittery movement and camera
What you see. The frame rate is high, but a moving character or the camera judders. It's worst on high refresh rate screens.
Why. Physics steps at a fixed rate, 50 times a second by default, while frames are drawn at the screen's rate. A Rigidbody drawn only where physics last put it lands in uneven places from one frame to the next, and a camera that follows it in Update or FixedUpdate makes that worse.
Set the Rigidbody's Interpolate to Interpolate. Unity then draws it between its last two physics positions, so it moves smoothly at any frame rate.
Move Rigidbodies in FixedUpdate with MovePosition(), velocity or forces, and read input in Update. Setting transform.position on a Rigidbody skips interpolation.
Move a follow camera in LateUpdate, after everything else has moved. With Cinemachine, leave the CinemachineBrain's Update Method on Smart Update, the recommended setting, which moves each camera in step with how its target moves.
Turn interpolation on for the player and whatever the camera follows, not every Rigidbody in the scene; it has a cost per body.
Mover.cs
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class Mover : MonoBehaviour
{
[SerializeField] float speed = 6f;
Rigidbody body;
Vector3 move;
void Awake()
{
body = GetComponent<Rigidbody>();
body.interpolation = RigidbodyInterpolation.Interpolate;
}
// Call from your input code, every frame.
public void SetMove(Vector2 input) => move = new Vector3(input.x, 0f, input.y);
void FixedUpdate() =>
body.MovePosition(body.position + Vector3.ClampMagnitude(move, 1f) * speed * Time.fixedDeltaTime);
}
Garbage collection spikes
What you see. A hitch every few seconds, often at a steady rhythm, with GC.Collect in the Profiler for that frame.
Why. Every new list or array, LINQ query, string you build, boxed value or lambda that captures a variable in Update makes garbage. When enough piles up, the garbage collector pauses the game to clean it.
Find the allocations in the Profiler (Hierarchy view, sorted by GC Alloc) and fix the ones that happen every frame. Reuse lists with Clear(), use the versions that fill a buffer you pass in (GetComponents(list), Physics.RaycastNonAlloc), keep LINQ and string building out of hot code, and cache GetComponent() results in Awake.
Keep Use Incremental GC on (Player Settings > Other Settings > Configuration), so collection is spread over several frames instead of one long pause. It's on by default in Unity 6; web builds don't support it, so allocations matter even more there.
Before and after
// Allocates a new array every frame:
// RaycastHit[] hits = Physics.RaycastAll(transform.position, transform.forward);
// Reuses one buffer:
readonly RaycastHit[] hits = new RaycastHit[16];
void Update()
{
int count = Physics.RaycastNonAlloc(transform.position, transform.forward, hits);
for (int i = 0; i < count; i++)
{
// use hits[i]
}
}
A hitch the first time something appears
What you see. A freeze the first time an effect, material or enemy shows up, and not the next time, with Shader.CreateGPUProgram in that frame.
Why. Unity prepares each shader variant for the GPU the first time it's drawn, and the frame waits. Newer graphics APIs also build a pipeline state for each mix of shader and render settings.
On DirectX 11 and OpenGL, record the variants the game uses into a ShaderVariantCollection and call WarmUp() on it while the loading screen is up. Project Settings > Graphics tracks the variants used while you play in the Editor and can save them to a new collection.
On DirectX 12, Vulkan and Metal that isn't enough: the driver can still stall when a draw uses a different vertex layout or render target. Unity 6 adds GraphicsStateCollection (still experimental, in UnityEngine.Experimental.Rendering), which records the actual pipeline states during a playthrough (BeginTrace() and EndTrace(), saved to a .graphicsState file) and warms them up during loading with WarmUp() or WarmUpProgressively().
Strip variants you never use (fewer shader keywords, and the stripping settings in Graphics settings), so there's less to compile.
ShaderWarmup.cs
using UnityEngine;
// Put on an object in the loading scene.
public class ShaderWarmup : MonoBehaviour
{
[SerializeField] ShaderVariantCollection variants;
void Start()
{
if (variants != null && !variants.isWarmedUp)
variants.WarmUp();
}
}
Hitches when things spawn
What you see. A hitch when a wave spawns, a big explosion goes off or a pile of pickups appears.
Why. Instantiate() builds the object, runs Awake() and OnEnable() and registers its colliders. Destroy() makes garbage. Dozens at once take a frame.
Create what a level needs during loading and reuse it through a pool. Draw many copies of the same thing with one particle system or GPU instancing instead of a GameObject each.
BulletPool.cs
using UnityEngine;
using UnityEngine.Pool;
public class BulletPool : MonoBehaviour
{
[SerializeField] GameObject prefab;
ObjectPool<GameObject> pool;
void Awake()
{
pool = new ObjectPool<GameObject>(
createFunc: () => Instantiate(prefab),
actionOnGet: b => b.SetActive(true),
actionOnRelease: b => b.SetActive(false),
actionOnDestroy: b => Destroy(b),
defaultCapacity: 100, maxSize: 300);
// Fill it while the level loads, not mid-fight.
var warm = new GameObject[100];
for (int i = 0; i < warm.Length; i++) warm[i] = pool.Get();
foreach (var b in warm) pool.Release(b);
}
public GameObject Get() => pool.Get();
public void Release(GameObject bullet) => pool.Release(bullet);
}
A freeze on a level change
What you see. A freeze of a second or more when a level starts.
Why. SceneManager.LoadScene() loads everything on the main thread, then runs every object's Awake() and Start() in the same frame. Resources.Load() of something large blocks the same way.
Load the next scene asynchronously, keep the loading screen animating, and let it activate when it's ready. Keep Awake() and Start() light in the new scene, and load big assets through Addressables or other async loads.
LevelLoader.cs
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class LevelLoader : MonoBehaviour
{
public void Load(string scene) => StartCoroutine(LoadRoutine(scene));
IEnumerator LoadRoutine(string scene)
{
AsyncOperation op = SceneManager.LoadSceneAsync(scene);
op.allowSceneActivation = false;
while (op.progress < 0.9f)
yield return null; // the loading screen keeps animating
op.allowSceneActivation = true;
}
}
Check the fix
Capture the same scene the same way before and after, in a Development Build on the same device, and compare the typical, 95th percentile and worst frames, not the average FPS. The Profile Analyzer package lines two captures up frame by frame.
A hitch fix should bring the worst frame down. A jitter fix shows in how movement looks, so record a few seconds on the same screen before and after.
Stutter that only your players see
Your machine isn't your players'. A shader variant that compiles quickly on your GPU can freeze a mid-range phone, and garbage that's harmless on a desktop can hitch a Steam Deck or an older laptop.
Glitchless is an open-source package for Unity 6.3 and later that times every frame of every player's session and names each hitch's likely cause (a garbage collection, a shader compile, a scene load, a burst of new objects), split by device and version, with errors and where players quit next to it. Your coding agent reads the same data and can check its own fix: run a build with --glitchless-measure=10 --glitchless-out=measure.json and it writes the numbers.
Ask your coding agent
Use Glitchless to find what makes my Unity game stutter for players: list the worst hitches by cause and scene, fix the top one, then measure a build with --glitchless-measure=10 to show the difference.
Frequently asked questions
Why does my game stutter in the Unity Editor but not in a build?
The Editor runs its own windows, the Scene view and extra safety checks next to your game. Profile a Development Build before you change anything; stutter that only happens in Play mode usually isn't what players see.
Should I use Application.targetFrameRate or V-Sync?
On desktop and web, QualitySettings.vSyncCount above 0 (the default is 1) makes Unity ignore targetFrameRate and wait for the screen, which gives the most even frames; Unity's docs warn that turning V-Sync off and capping with targetFrameRate can cause small stutters. On Android and iOS it's the other way round: vSyncCount is ignored, and the default targetFrameRate of -1 means 30 fps, so set it to 60, or the screen's rate, if your game should run faster.
Does garbage collection always cause stutter?
Only when it has a lot to collect at once. With incremental GC on and little garbage made each frame, collections are short and spread out. The goal is 0 B allocated per frame in normal play.
Why does the stutter only happen the first time?
Something is being prepared the first time it's used: a shader variant, a pipeline state, an asset being loaded or an object pool being filled. Do that work behind a loading screen instead, with the warm-up and pooling above.
Sources
- Unity docs: Rigidbody.interpolation
- Unity manual: Incremental garbage collection
- Unity docs: ShaderVariantCollection.WarmUp
- Unity docs: ObjectPool
- Unity docs: SceneManager.LoadSceneAsync
- Unity docs: Application.targetFrameRate
- Unity manual: Shader and PSO warm-up
- Cinemachine manual: Cinemachine Brain
Something wrong or out of date? Email support@glitchless.gg and we'll fix it. Glitchless isn't affiliated with Unity Technologies; product names are their owners' trademarks.