# Unity stuttering fix: find the cause first

> How to fix stuttering in a Unity game: jittery movement and camera (Rigidbody interpolation, LateUpdate, Cinemachine), garbage collection spikes, shader variant hitches, Instantiate bursts and scene loads, with C# you can copy and a way to check the fix.

Website: https://www.glitchless.gg/unity-stuttering-fix.html · 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

1. **Jittery movement and camera**: turn on Rigidbody interpolation, move bodies in `FixedUpdate`, and move the camera in `LateUpdate`
2. **Garbage collection spikes**: stop allocating every frame, and keep incremental GC on
3. **A hitch the first time something appears**: warm up shader variants behind the loading screen
4. **Hitches when things spawn**: pool objects with `UnityEngine.Pool.ObjectPool` instead of `Instantiate` and `Destroy`
5. **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:

```csharp
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:

```csharp
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:

```csharp
// 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:

```csharp
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:

```csharp
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:

```csharp
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](https://www.glitchless.gg/index.md) 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](https://docs.unity3d.com/ScriptReference/Rigidbody-interpolation.html)
- [Unity manual: Incremental garbage collection](https://docs.unity3d.com/Manual/performance-incremental-garbage-collection.html)
- [Unity docs: ShaderVariantCollection.WarmUp](https://docs.unity3d.com/ScriptReference/ShaderVariantCollection.WarmUp.html)
- [Unity docs: ObjectPool](https://docs.unity3d.com/ScriptReference/Pool.ObjectPool_1.html)
- [Unity docs: SceneManager.LoadSceneAsync](https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.LoadSceneAsync.html)
- [Unity docs: Application.targetFrameRate](https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Application-targetFrameRate.html)
- [Unity manual: Shader and PSO warm-up](https://docs.unity3d.com/6000.3/Documentation/Manual/shader-pso-introduction.html)
- [Cinemachine manual: Cinemachine Brain](https://docs.unity3d.com/Packages/com.unity.cinemachine@3.1/manual/CinemachineBrain.html)
