【Unity】Getting Started with the Unity Profiler: Finding the Culprit Behind Lag with Data

Created: 2025-12-07Last updated: 2026-07-12

Your game stutters, but you can't tell what's slow — the golden rule of optimization is "Don't guess — measure." This guide covers the practical workflow for identifying spike culprits with the Profiler (CPU Usage, sorting by Self ms, the GC Alloc column) all the way to profiling on real devices.

Your game stutters. You try removing objects, commenting out chunks of Update(), turning off shadows — and it still isn't fixed. This "delete things at random and hope" approach fails because you're hunting for the culprit by guesswork.

The golden rule of performance optimization fits in one line: "Don't guess — measure." And the measuring tool for the job ships with Unity: the Profiler.

Profiler concept art. A clay figure looks up at a performance graph where a single red spike stands out, magnified by a large blue magnifying glass

What You'll Learn

  • How to approach optimization — the measure → identify → fix → re-measure loop
  • Profiler basics (Record, selecting frames)
  • How to read the CPU Usage module — finding culprits with Self ms sorting and the GC Alloc column
  • Common patterns of expensive work and the articles that cover each fix
  • How to do on-device profiling (Development Build)

Sponsored

The Golden Rule: Don't Guess — Measure

Optimization works as a loop.

Diagram of the optimization loop. Measure (record with the Profiler) → Identify (find the heaviest work) → Fix (change only that one spot) → Re-measure (confirm the effect with numbers), arranged in a circle
  1. Measure: Record with the Profiler and find spikes (tall peaks in the graph)
  2. Identify: Pin down the process eating the most time in that frame
  3. Fix: Change only the single heaviest spot
  4. Re-measure: Confirm the improvement with numbers

The key is to fix only one thing at a time. If you fix several things at once, you can't tell which change helped (or which one made things worse). Also, no matter how much you speed up something that only accounts for 1% of the frame, the game won't feel even 1% faster. Always start with whatever is heaviest right now.

Profiler Basics

  1. Open it: Choose "Window > Analysis > Profiler" from the menu (shortcut: Ctrl + 7).
  2. Record: With the "Record" button enabled, play the game and the graph starts scrolling in real time.
  3. Select a frame: On the graph, click the spike (tall peak) at the moment the game stuttered. The breakdown of that frame's work appears in the lower part of the window.

tips When scanning the graph, look at the color legend first. The colors making up a spike (Scripts = light blue means code, Rendering = green means drawing, GC = yellow means memory) narrow down the culprit's category at a glance.

CPU Usage — Where the Manhunt Happens

When investigating stutters, start with the CPU Usage module. After clicking the spiking frame, switch the lower view to Hierarchy.

Diagram of finding the culprit in CPU Usage. Clicking a spike in the graph opens a list of processes, with the most time-consuming row highlighted

Every process executed in that frame is shown as a hierarchical list. You only need three columns.

ColumnMeaningHow to use it
Self msTime spent in that process itselfSort descending here → the top entries are your culprits
Time msTotal time including child processesFor grasping the big picture
GC AllocHeap memory allocated in that frameNonzero every frame means fuel for GC spikes

The routine is always the same: "Click the spike → open Hierarchy → sort Self ms descending → suspect the top entry." Your own scripts show up under names like ScriptName.Update(), so expand the hierarchy to drill down to the specific method.

tips To measure a specific section of your own code, wrap it in UnityEngine.Profiling.Profiler.BeginSample("EnemyAI") and Profiler.EndSample(). It shows up in the Hierarchy as a named row called "EnemyAI", letting you narrow things down to "which part of Update is actually slow."

Sponsored

Common Bottleneck Patterns

Here are the "usual suspects" that frequently top the Hierarchy, along with the articles covering each fix.

Diagram of the five usual heavy-process suspects that appear in the Profiler. Huge GC.Collect spike → GC countermeasures; frequent Instantiate/Destroy → object pooling; your Update() near the top → cache GetComponent; too many Batches → batching; heavy Physics.Simulate → simplify Colliders
What it looks like in the ProfilerCulpritFix
Huge GC.Collect spikesPer-frame heap allocationsGC optimization
Frequent Instantiate / DestroyMass spawning/destroying of bullets, enemies, effectsObject pooling
Your own script's Update() near the topGetComponent in loops, expensive searches, per-frame string workCaching GetComponent
Heavy Camera.Render, many BatchesToo many draw callsDraw calls and batching
Heavy Physics.SimulateToo many Colliders, overuse of Mesh CollidersSimplify collision shapes, revisit your FixedUpdate settings

Practical: Hunting Down and Killing a GC Spike

Let's run through everything so far on a case you'll actually hit. "A periodic stutter, once every few seconds"—a textbook GC-spike symptom that plagues shmups and RPGs alike.

  1. Measure: Turn on Record, play, and click the red spike at the moment it stutters.
  2. Identify: Open Hierarchy and GC.Collect is hogging tens of milliseconds. But GC.Collect is "the moment of cleanup," not the true culprit. Re-sort by the GC Alloc column and there it is—ScoreUI.Update() was allocating 2.3 KB every frame. The contents: a "Score: " + score string concatenation.
  3. Fix: Stop generating a string every frame. Just update text only when the score changes (make it event-driven) and this allocation drops to zero.
  4. Re-measure: Record the same scene and confirm with numbers that GC Alloc is now 0 B and the periodic spikes are gone.
Before/after comparison of fixing a GC spike. Before, there are periodic red spikes and GC Alloc of 2.3 KB/frame; after stopping the string generation, the spikes disappear and GC Alloc becomes 0 B

There are two key points.

  • GC.Collect isn't the culprit—it's the "result": Staring at the spike row itself won't fix anything. The right way to track it down is to hunt in the GC Alloc column for "the thing allocating every frame" (the mechanism is covered in the GC optimization article).
  • The go-to fix is "call it less often": Rather than speeding up the string concatenation itself, it's more effective to question "does this really need to happen every frame?" Updating only when the value changes is also the doorway to event-driven design.

Profiling on a Real Device

Measurements in the Editor are rough estimates, because the Editor's own overhead gets mixed in. They're fine for spotting trends, but the final call on "does it actually hit the frame budget?" must always happen on a real device. This matters especially on mobile, where the CPU/GPU performance balance is completely different from a PC.

  1. In "File > Build Profiles" (formerly Build Settings), enable "Development Build" and "Autoconnect Profiler", then build.
  2. Launch the game on the device, and its data streams into the Profiler in the Editor over the same network (or a USB connection).
  3. If it doesn't connect, pick the device directly from the target selector at the top of the Profiler window (the Playmode dropdown).

Other Modules

  • Rendering: Shows draw calls (Batches, SetPass Calls) and polygon counts. Your entry point when rendering is the bottleneck.
  • Memory: Shows overall memory usage. To dig into "which texture or asset is eating memory," the Memory Profiler package introduced in the bonus section is far more powerful. How assets end up in memory in the first place is covered in the asset management article.
  • GPU Usage: Measures time spent on the GPU (unavailable on some platforms). If trimming CPU work doesn't change your frame rate, you may be GPU bound.

Bonus: Good to Know for Later

  • Deep Profile: A mode that records every single method call. It pinpoints culprits precisely, but the measurement itself becomes extremely heavy and skews the numbers, so it's best used briefly — "turn it on for a moment to see the structure."
  • Frame Debugger: "Window > Analysis > Frame Debugger". Lets you replay a single frame's rendering one draw call at a time, and even tells you why batching was broken. Your best friend for rendering optimization.
  • Memory Profiler package: A separate tool installable from the Package Manager. It takes memory snapshots and visualizes "what's occupying how much" as a treemap. This is the go-to for investigating out-of-memory crashes.
  • Profiler Analyzer: An official package for statistical comparison across multiple frames. It lets you compare "before vs. after a fix" using averages, making your re-measurements more reliable.

Summary

  • Don't fix by guesswork. Follow the measure → identify → fix → re-measure loop, one change at a time.
  • The basic stutter-hunting routine is "click the spike → Hierarchy → sort Self ms descending."
  • A value showing up in the GC Alloc column every frame is an early warning of a GC spike. GC.Collect is the result, not the culprit.
  • Make the final call with a Development Build on a real device. Editor numbers are estimates.
  • The common patterns (GC, Instantiate, draw calls) each have a well-established fix.

The Profiler isn't a tool you "open and stare at" — it's a tool that points at the culprit for you. When the game feels slow, open it before you start deleting things.