Profiling GDScript is one of those things every Godot developer eventually learns the hard way. The first time you ship a build that drops to 22 fps on a mid-tier laptop, you discover the profiler exists, you open it, and the numbers stare back at you with no obvious next step.
This is the guide I wish I had when I started. It covers what changed in Godot 4.6's unified docking, how to use the built-in profiler properly, when to reach for external tracing profilers like Tracy, and the four common GDScript bottlenecks that cause most of the frame-time damage.
Three things I got wrong before I read the docs
Profiling in the editor. The Godot editor adds overhead to every frame, so the numbers you see when you press F6 inside the editor are not the numbers your players see. Godot's own docs are explicit about this: "for accurate performance numbers, profile an exported build". Half the optimizations I "made" in 2024 were undoing imaginary problems.
Treating frame time as one number. Frame time has at least three layers: CPU work in your scripts, CPU work in the engine, and GPU work in the renderer. The Monitor tab gives you a single FPS number that mashes all three together. The Profiler tab is what splits it.
Not enabling typed code. The single largest source of GDScript performance complaints I see on the forums is untyped code. From the Godot performance docs on CPU optimization: "Untyped variables require the runtime to determine the type and dispatch the correct operation on every operation, while typed variables skip this resolution." Same for arrays. If your hot loops use var arr = [] instead of var arr: Array[int] = [], that is your low-hanging fruit before you ever touch the profiler.
Step 1: Open the right panel
The profiling tools live in the Debugger panel at the bottom of the editor. With Godot 4.6, the Debugger is now a regular dock that can be moved or floated, which is useful if you have a vertical monitor and want the profiler in a side column.
Three tabs matter for performance:
- Profiler. Per-script function call timings, sorted by self time or total time. This is where you find the slow function.
- Monitors. Live graphs of FPS, memory, draw calls, physics steps, and more. Good for catching trends over a play session.
- Visual Profiler. Per-frame breakdown of the renderer cost. Godot 4.7 dev 4 added folding support to the Visual Profiler tree, which makes it usable on complex frames.
The Profiler does not record by default. You have to click "Start" before you reproduce the slow scenario, and "Stop" when you are done. The docs note that profiling is performance-intensive because it instruments every frame, so leaving it on the whole session will distort the readings of the very thing you are measuring.
Step 2: Find the slow function
Sort the Profiler results by Self Time first, not Total Time. Self Time is what the function itself does, excluding everything it calls. Total Time can be misleading: a function that calls a slow library will show high Total Time but the fix is downstream.
Common patterns I see in real Godot 4 projects:
- A
_processcallback that runsget_tree().get_nodes_in_group(...)every frame. - A
_physics_processdoing string concatenation for debug output that ships in release builds. - A loop that calls
Vector2.distance_toinstead ofdistance_squared_tofor a comparison check. - A signal connected in
_readythat fires multiple times per frame because it was also connected in_enter_tree.
The profiler tells you which function. Reading the function tells you which of these patterns it is.
Step 3: Use custom monitors for things the built-in graphs miss
The built-in Monitors tab has roughly 30 metrics. None of them know what your game does. If you want to track "active enemies on screen" or "outstanding network requests" or "items in the loot pool," you have to add a custom monitor.
The API is straightforward. From the custom performance monitors docs:
extends Node
func _ready() -> void:
Performance.add_custom_monitor("game/active_enemies", _count_enemies)
Performance.add_custom_monitor("game/loot_drops_per_minute", _loot_rate)
func _count_enemies() -> int:
return get_tree().get_nodes_in_group("enemies").size()
func _loot_rate() -> float:
return loot_log.size() / max(0.01, time_played_minutes)
The metric appears under your category in the Monitors tab next session. Custom monitors are the cheapest way to validate that what you think your game is doing matches what it actually does.
Step 4: Reach for external tracers when the built-in profiler isn't enough
Godot 4.6 added support for tracing profilers like Tracy, Perfetto, and Apple Instruments. These give you per-frame, per-thread, microsecond-level visibility into the engine internals, useful when the bottleneck is in the renderer or physics step rather than your script.
When to reach for external profilers:
| Symptom | Try first |
|---|---|
| Slow function in your code | Built-in Profiler tab |
| FPS drops on specific scenes | Visual Profiler tab |
| Stutters that don't show up in script timings | Tracy or Perfetto |
| Mac-specific performance problem | Apple Instruments |
| Frame pacing or VRR issues | RenderDoc or PIX (per platform) |
Tracy is the one most commonly reported as worth the setup cost for indie projects. The build needs to be compiled with the Tracy hooks enabled, which is heavier than the built-in tools, but the resolution is in a different league.
Step 5: Fix the four GDScript patterns that account for most of the damage
After you have a profiler trace and you know which function is slow, the fix is usually one of four things.
Untyped variables and arrays. Add types. var hp: int = 100 is faster than var hp = 100. var enemies: Array[Enemy] = [] is faster than var enemies = []. The cost is one annotation per declaration. The benefit is the runtime skipping the type-resolve step on every access.
get_node, get_tree, get_nodes_in_group in hot loops. These walk the scene tree on every call. Cache the result in _ready and store it as @onready var hud: HUD = $UI/HUD. Same for get_tree().get_root() and group lookups.
String operations in _process. str(x) + " " + str(y) allocates two strings per frame. Use "%d %d" % [x, y] if you need formatting, or move the string work behind an if Engine.is_editor_hint() so it only runs in debug.
Signal duplication. connect() does not deduplicate. If _ready and _enter_tree both connect to the same signal, the slot fires twice per emit. The fix is to check is_connected() first, or only connect in one lifecycle hook.
Step 6: Read the profile, not the code, for confirmation
The discipline that took me longest to internalize: do not optimize from intuition. Profile, change one thing, profile again, confirm the number moved in the direction you expected. Half the "optimizations" that look obvious in code do nothing for frame time, and a few make it worse.
If you have an AI assistant in your editor, this is one of the few places where it can pull weight without making things worse. Ask it to read the profiler dump and the slow function side by side and propose a hypothesis. Tools like Ziva that run inside Godot can read the actual scene tree and the actual project structure, which matters here because most GDScript performance problems are about how a script interacts with the rest of the project, not about the script in isolation. Generic chat tools that only see the script text miss the cross-cutting issues.
What to skip
A few things are not worth your time at the start:
- Premature
statictyping of every single variable in your codebase. Type the hot loops first. - Refactoring
Vector2math to use a custom struct. The engine version is fine; your bottleneck is elsewhere. - Replacing GDScript with C# for "performance." GDScript and C# benchmarks are competitive in Godot 4 for most game logic. C# helps when you have CPU-bound math kernels. It does not help when your problem is
get_nodes_in_groupin_process.
The two-line summary: profile exported builds, type your hot loops, cache scene tree lookups in _ready, and use external tracers only after the built-in profiler tells you the bottleneck is not in your code. Most performance problems in shipped Godot projects are one of these four things, and the profiler will tell you which one if you let it.
Top comments (0)