بنقرة واحدة
godot-optimization
// Use when optimizing Godot games — profiler, draw calls, physics tuning, memory management, and common bottlenecks
// Use when optimizing Godot games — profiler, draw calls, physics tuning, memory management, and common bottlenecks
Use when building native extensions for Godot — godot-cpp (C++) or gdext (Rust), binding classes, building, and GDScript/C# interop
Use when targeting Android/iOS — export and signing, permissions, plugins, in-app purchases, ads, app lifecycle, device features, and mobile performance
Use when running work off the main thread — WorkerThreadPool, Thread/Mutex/Semaphore, call_deferred, thread-safe scene access, and threaded resource loading
Use when importing and managing assets — image compression, 3D scene import, audio formats, resource formats, and import configuration
Use when working with C# in Godot — conventions, GodotSharp API differences from GDScript, project setup, and interop
Use when exporting and distributing Godot games — export presets, platform settings, CI/CD with GitHub Actions
| name | godot-optimization |
| description | Use when optimizing Godot games — profiler, draw calls, physics tuning, memory management, and common bottlenecks |
This skill covers performance optimization for Godot 4.3+ projects in both GDScript and C#. It covers the built-in profiler, draw call reduction, physics tuning, GDScript performance patterns, memory management, object pooling, and a reference table of common bottlenecks.
Related skills: godot-debugging for systematic debugging and profiling, godot-code-review for performance review checklist, export-pipeline for release build optimization, physics-system for collision shapes, layers, and physics body types, 2d-essentials for 2D mesh optimization, particle performance, and draw order tuning, multithreading for moving work off the main thread, mobile-development for mobile performance budgets.
At 60 fps, the entire frame (update, physics, rendering) must complete in 16.6 ms. At 30 fps the budget is 33.3 ms. Any single system that consumes the majority of that budget is a bottleneck.
| Target FPS | Frame budget |
|---|---|
| 120 | 8.3 ms |
| 60 | 16.6 ms |
| 30 | 33.3 ms |
Open Debugger > Profiler, click Start, play through the scenario you want to measure, then click Stop.
# Manual micro-benchmark for a specific block
var start := Time.get_ticks_usec()
_run_expensive_operation()
var elapsed := Time.get_ticks_usec() - start
print("_run_expensive_operation: %d µs" % elapsed)
C#:
// Manual micro-benchmark using Stopwatch (high-resolution timer)
using System.Diagnostics;
var sw = Stopwatch.StartNew();
RunExpensiveOperation();
sw.Stop();
GD.Print($"RunExpensiveOperation: {sw.Elapsed.TotalMilliseconds:F3} ms");
// Alternative using Godot's built-in timer (microsecond precision)
long start = (long)Time.GetTicksUsec();
RunExpensiveOperation();
long elapsed = (long)Time.GetTicksUsec() - start;
GD.Print($"RunExpensiveOperation: {elapsed} µs");
Debugger > Monitors shows real-time engine metrics while the game is running. Click a monitor name to open a live graph. Key monitors to watch:
| Monitor | What to watch for |
|---|---|
Time > FPS | Below target — frame budget overrun |
Time > Process | High — _process() callbacks are expensive |
Time > Physics Process | High — _physics_process() or physics sim is expensive |
Render > Total Draw Calls | Above ~500 (mobile) or ~2 000 (desktop) — needs batching |
Render > Video RAM | Steadily growing — unfreed textures or meshes (memory leak) |
Object > Object Count | Growing across scene reloads — nodes are not being freed |
Physics 3D > Active Bodies | Large count in simple scenes — bodies not sleeping |
# Query any monitor at runtime from code
var fps := Performance.get_monitor(Performance.TIME_FPS)
var draw_calls := Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
var video_ram := Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED)
print("FPS: %d | Draw calls: %d | VRAM: %.1f MB" % [fps, draw_calls, video_ram / 1_048_576.0])
C#:
// Query any monitor at runtime from code
double fps = Performance.GetMonitor(Performance.Monitor.TimeFps);
double drawCalls = Performance.GetMonitor(Performance.Monitor.RenderTotalDrawCallsInFrame);
double videoRam = Performance.GetMonitor(Performance.Monitor.RenderVideoMemUsed);
GD.Print($"FPS: {fps:F0} | Draw calls: {drawCalls:F0} | VRAM: {videoRam / 1_048_576.0:F1} MB");
Every distinct mesh, sprite, or canvas item that cannot be batched with its neighbours costs one draw call. Reducing draw calls is one of the highest-leverage optimisations, especially on mobile — wrap 2D groups sharing a texture in CanvasGroup, keep unique-material count low, atlas sprites, and cull off-screen work.
See references/draw-calls.md for the full recipes (CanvasGroup batching constraints, shared shader-parameter materials, texture atlases,
VisibleOnScreenNotifier2D/3Dculling, and 3D LOD swapping).
Physics tuning hinges on minimising broadphase work and avoiding mesh colliders on moving bodies. Trim collision masks to only the layers each body actually needs, replace ConcavePolygonShape3D with primitives on anything that moves, and prefer Area2D/3D over per-frame raycasts for overlap detection.
See references/physics-tuning.md for the full recipes (layer/mask bit examples, collision-shape cost table,
Engine.physics_ticks_per_secondtuning, Area-vs-raycast patterns).
Hot-path GDScript wins come from eliminating per-frame allocations, comparing StringName instead of String, using typed arrays / PackedArrays, and preload-ing resources at class scope. The same allocation discipline applies to C# (with List<T> in place of typed Array[T] and static readonly StringName fields).
See references/cpu-bottlenecks.md for the full recipes (cached group queries, reused vector locals,
&"..."literals,PackedVector2Array, static typing,preloadvsload, plus C# parity blocks).
Watch Performance.MEMORY_STATIC and OBJECT_COUNT across scene reloads — steady growth means leaked references. Resources loaded by path are cached and shared; call .duplicate() when you need per-instance mutation. Always prefer queue_free() for nodes; free() inside a self-emitted signal will crash.
See references/memory-management.md for the full recipes (Performance singleton queries, ResourceLoader cache semantics,
queue_freevsfreetable, plus the full GDScript and C# object pool implementations for bullets/effects/particles).
| Problem | Diagnosis tool | Fix |
|---|---|---|
| Too many draw calls | Debugger > Monitors Render > Total Draw Calls; Viewport > Debug > Draw Calls overlay | Use CanvasGroup for 2D batching; merge meshes for 3D; use texture atlases; reduce unique materials |
Heavy GDScript in _process | Profiler > Self column shows script functions at top | Move logic to _physics_process (runs less often), cache queries, avoid per-frame allocations, consider C# for tight loops |
| Excessive signal connections | Profiler shows signal dispatch overhead; manually audit get_signal_connection_list() | Remove redundant connections; prefer polling over per-frame signals for high-frequency data; use CONNECT_ONE_SHOT for fire-and-forget |
| Unoptimised TileMap | Profiler shows TileMap._process or high draw call count | Split into fewer layers; use a single atlas texture per layer; disable use_parent_material if not needed; use TileMapLayer (Godot 4.3+) instead of legacy TileMap |
| Large uncompressed textures | Monitors Render > Video RAM is high; check Import dock for texture settings | Enable texture compression (VRAM Compressed) in the Import dock; use mipmaps; halve resolution of assets not viewed up-close |
| Too many active physics bodies | Monitors Physics 3D > Active Bodies is high; slow _physics_process in Profiler | Enable sleeping on RigidBody3D (can_sleep = true); lower physics tick rate; replace distant bodies with fake animations; use layers/masks to narrow collision checks |
| String operations in hot paths | Profiler shows String allocation functions; high GC pressure | Replace String comparisons with StringName (&"..."); avoid String formatting in _process; build strings once and cache |
instantiate() in hot paths | Profiler shows PackedScene.instantiate with high Self time | Implement object pooling (see references/memory-management.md); preload scenes at startup; spawn during loading screens rather than during gameplay |
_process — new Arrays, Dictionaries, Strings, or Vector constructors per frame. Cache the container, mutate fields in place. See references/cpu-bottlenecks.md.ConcavePolygonShape3D on a CharacterBody3D or RigidBody3D. Use a capsule, box, or convex hull instead. See references/physics-tuning.md.material_override = SomeMaterial.new() in _ready() breaks batching. Share one material; vary via shader parameters. See references/draw-calls.md.load() in hot paths — calling load("res://...") from _process or _physics_process. Use const X := preload(...) at class scope. See references/cpu-bottlenecks.md.instantiate() + queue_free() for short-lived objects — bullets, hit effects, particles. Pool them. See references/memory-management.md.Work through this list before shipping or when investigating a performance complaint.
Profiler
Draw Calls
CanvasGroup.VisibleOnScreenNotifier2D/3D to pause processing.Physics
ConcavePolygonShape — replaced with capsule, box, or convex.Area2D/3D is used for overlap detection instead of per-frame raycasts.RigidBody3D nodes have can_sleep = true where applicable.GDScript
Array, Dictionary, or String is allocated inside _process or _physics_process.StringName (&"...").Array[T] or PackedArray).preload at class scope, not load per frame.Memory
Performance.get_monitor(Performance.MEMORY_STATIC) is stable between scene reloads..duplicate()d.queue_free() unless synchronous teardown is explicitly required.Object Pooling