Quick answer: In Godot 4, open the Profiler from the Debugger panel at the bottom of the editor. Click the Debugger tab, then select the Profiler sub-tab. Press the Start button before running your scene. The profiler records function call times and displays them in a timeline.
This Godot performance profiling guide for beginners covers everything you need to know. Godot makes it remarkably easy to prototype a game, but that speed sometimes means performance problems sneak in unnoticed until the project grows. Your 2D platformer ran fine with ten enemies, but now with fifty the frame rate tanks. Godot ships with built-in profiling tools that most beginners never open. This guide walks through every tool available, explains what the numbers mean, and shows you how to fix the most common GDScript performance mistakes.
The Debugger Panel: Your Starting Point
Godot's profiling tools live in the Debugger panel at the bottom of the editor. This panel has several tabs, but the three that matter for performance are Profiler, Monitors, and Misc. Most beginners never click past the Errors tab, which means they miss the most useful diagnostic tools the engine provides.
Before running your scene, open the Debugger panel and click the Profiler tab. Click Start to begin recording. Now run your scene. The profiler captures function-level timing data and presents it as a timeline and a sortable table. When you see the frame rate drop, you can scrub through the timeline to that exact moment and inspect which functions consumed the most time.
Understanding Monitors
The Monitors tab shows real-time engine metrics. These are the vital signs of your game. Enable the ones that matter most for performance investigation:
Time
FPS # Frames per second (target: 60)
Process Time # Time spent in _process callbacks (ms)
Physics Process Time # Time spent in _physics_process callbacks (ms)
Rendering
Draw Calls # Number of draw calls per frame
2D Items # Number of 2D items being drawn
Vertices # Total vertex count submitted to GPU
Object
Node Count # Total nodes in the scene tree
Orphan Nodes # Nodes not in the tree (potential leak)
Object Count # Total objects in memory
Watch these numbers while playing your game. If the node count keeps climbing when it should be stable, you are leaking nodes. If draw calls spike when entering a particular area, that area has too many unique materials or unculled objects. If process time is high but physics process time is low, your performance problem is in _process rather than physics.
_process vs _physics_process: Know the Cost
These two callbacks behave differently and have different performance characteristics. _process runs once per rendered frame. At 60 FPS, that is 60 calls per second. At 144 FPS on a high-refresh monitor, that is 144 calls per second. Expensive logic in _process scales with frame rate, which means your game runs worse on faster hardware — the opposite of what players expect.
# BAD: Expensive calculation runs every rendered frame
func _process(delta: float) -> void:
var nearest_enemy := find_nearest_enemy() # O(n) search
var path := nav_agent.get_next_path_position()
update_healthbar_positions() # Iterates all enemies
# GOOD: Separate concerns by frequency
func _ready() -> void:
var timer := Timer.new()
timer.wait_time = 0.25 # 4 times per second, not 60
timer.timeout.connect(_update_ai)
add_child(timer)
timer.start()
func _process(delta: float) -> void:
# Only visual updates that need per-frame smoothness
position = position.lerp(target_position, delta * speed)
func _update_ai() -> void:
nearest_enemy = find_nearest_enemy()
nav_agent.target_position = nearest_enemy.global_position
_physics_process runs at a fixed rate (default 60 Hz, configurable in Project Settings). It is deterministic and frame-rate independent. Physics code, collision responses, and movement that must be consistent belong here. But do not put rendering or animation code in _physics_process — it will appear jittery if the render rate differs from the physics rate.
Common GDScript Performance Pitfalls
GDScript is an interpreted language. It is fast enough for most game logic, but certain patterns are dramatically slower than their alternatives. The profiler will reveal these, but knowing them in advance saves time.
Untyped variables. Every operation on an untyped variable requires the runtime to determine the type, check compatibility, and dispatch the correct operation. Typed variables skip this resolution.
# SLOW: Runtime must resolve types on every operation
var speed = 5.0
var direction = Vector2.ZERO
var velocity = direction * speed
# FAST: Types known at parse time, operations are direct
var speed: float = 5.0
var direction: Vector2 = Vector2.ZERO
var velocity: Vector2 = direction * speed
Untyped arrays. Iterating over an untyped array requires a type check on every element access. Typed arrays in Godot 4 eliminate this overhead.
# SLOW: Each access checks the element type
var enemies = []
for enemy in enemies:
enemy.take_damage(10)
# FAST: Type is known, no per-element checking
var enemies: Array[Enemy] = []
for enemy: Enemy in enemies:
enemy.take_damage(10)
Repeated get_node calls. Every get_node or $NodeName call traverses the scene tree. In a per-frame callback, this adds up.
# BAD: Traverses scene tree 60 times per second
func _process(delta: float) -> void:
$Sprite2D.rotation += delta
$HealthBar.value = health
# GOOD: Cache references once at initialization
@onready var sprite: Sprite2D = $Sprite2D
@onready var health_bar: ProgressBar = $HealthBar
func _process(delta: float) -> void:
sprite.rotation += delta
health_bar.value = health
Creating nodes in loops. Instantiating nodes is expensive. If you spawn and destroy bullets, enemies, or particles frequently, use object pooling. Create the nodes once at startup, hide and deactivate them when not in use, and reactivate them when needed.
Using the Servers API for Bulk Operations
When the scene tree becomes a bottleneck — typically with hundreds of similar objects — consider using Godot's servers API directly. The servers (RenderingServer, PhysicsServer2D, PhysicsServer3D) operate without the scene tree overhead and are significantly faster for bulk operations.
# Drawing 1000 sprites using RenderingServer directly
# Much faster than 1000 Sprite2D nodes
var texture_rid: RID
var canvas_items: Array[RID] = []
func _ready() -> void:
var texture: Texture2D = preload("res://bullet.png")
texture_rid = texture.get_rid()
for i in 1000:
var ci: RID = RenderingServer.canvas_item_create()
RenderingServer.canvas_item_set_parent(ci, get_canvas_item())
RenderingServer.canvas_item_add_texture_rect(
ci, Rect2(0, 0, 16, 16), texture_rid)
canvas_items.append(ci)
func _process(delta: float) -> void:
for i in canvas_items.size():
var t := Transform2D.IDENTITY
t.origin = positions[i]
RenderingServer.canvas_item_set_transform(canvas_items[i], t)
This pattern is ideal for bullet hell games, particle-heavy effects, and any situation where you need hundreds or thousands of similar visual elements. The trade-off is more complex code and manual resource management.
Profiling Exported Builds
The Godot editor adds overhead to every frame. For accurate performance numbers, profile an exported build. Export a debug build (not release) and run it from the command line with the --remote-debug flag to connect the editor's debugger to the running game.
# Export a debug build and profile remotely
$ ./my_game.x86_64 --remote-debug tcp://127.0.0.1:6007
# In Editor: Debugger > Profiler will connect to the running game
# and show accurate timing data without editor overhead
Compare your profiling results between the editor and the exported build. Methods that appear expensive in the editor may be fine in the exported build, and vice versa. Always make optimization decisions based on exported build data.
Related Resources
For memory-specific issues, see how to find memory leaks in Godot games. For GPU profiling across all engines, read GPU profiling for game developers. For tracking performance issues alongside bug reports, explore bug reporting tools for Godot developers.
Add type annotations to every variable and function signature in one script file today. Run the profiler before and after. The difference in a hot loop will surprise you.