--- name: godot-performance description: "Performance optimization for Godot 4.x: profiling workflow (built-in profiler, CPU/GPU usage), memory management with ResourceCache and instance pooling, instancing optimization with packed scenes and LOD, render passes, draw calls, mobile-specific optimizations, Perfetto tracing, and nearest-neighbor viewport scaling. Use when debugging performance, optimizing games, or targeting low-end hardware." license: MIT compatibility: - godot-4.0 - godot-4.1 - godot-4.2 - godot-4.3 - godot-4.4 - godot-4.5 - godot-4.6 - godot-4.7 metadata: author: vl4dt version: 0.1.0 tags: - godot - performance - profiling - optimization - memory - mobile created: 2026-06-20 --- # Performance Optimization for Godot 4.x Systematic approach to profiling, optimizing, and debugging performance in Godot 4.x. ## Profiling Workflow ### Built-in Profiler Access the profiler via Debug > Start Profiling or press Ctrl+Shift+P. ```gdscript # Enable profiling in your project settings: # Project Settings > Debug > Profile Every Frame = true # Use time measure for specific code sections var start_time = Time.get_ticks_usec() # ... expensive operation ... var elapsed = Time.get_ticks_usec() - start_time print("Operation took: ", elapsed, " us") ``` ### CPU/GPU Usage Monitoring ```gdscript # Display FPS and frame time in debug overlay # Project Settings > Debug > Show FPS = true # Monitor specific metrics func get_performance_stats(): var stats = { fps: Performance.get_monitor(Performance.TIME_FPS), frame_time: Performance.get_monitor(Performance.TIME_FRAME_DURATION), draw_calls: Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME), active_objects: Performance.get_monitor(Performance.OBJECT_COUNT), mem_alloc: Performance.get_monitor(Performance.MEMORY_STATIC), mem_pool: Performance.get_monitor(Performance.MEMORY_STATIC_SPECIALIZATION) } return stats ``` ### Profiling Checklist 1. **Start with a clean build** — Profile the exported release build, not the debug version 2. **Enable all profiler categories** — CPU, GPU, Physics, Rendering, Audio 3. **Record a representative gameplay segment** — 30-60 seconds of typical play 4. **Identify hotspots** — Look for functions consuming >5% of frame time 5. **Profile before and after changes** — Measure impact of each optimization ## Memory Management ### ResourceCache Pattern Preload and cache frequently-used resources. ```gdscript class_name ResourceCache extends Node static var _cache := {} static func preload_resource(path: String) -> Variant: if not _cache.has(path): var res = load(path) if res: _cache[path] = res else: push_warning("Failed to load resource: " + path) return _cache[path] static func get_scene(path: String) -> PackedScene: return preload_resource(path) as PackedScene # Usage var bullet_scene = ResourceCache.get_scene("res://scenes/entities/bullet.tscn") var bullet = bullet_scene.instantiate() ``` ### Instance Pooling Pattern Reuse objects instead of creating/destroying them. ```gdscript class_name ObjectPool extends Node where T is RefCounted @export var pool_size: int = 20 var _pool: Array[T] = [] var _active: Array[T] = [] func _ready(): for i in range(pool_size): var instance = create_instance() instance.set_process_mode(PROCESS_MODE_ALWAYS) add_child(instance as Node) _pool.append(instance as T) func get_free() -> T: if _pool.size() > 0: var obj = _pool.pop_back() obj.set_active(true) _active.append(obj) return obj return null func release(obj: T) -> void: obj.set_active(false) _active.erase(obj) _pool.append(obj) ``` ## Instancing Optimization ### Packed Scene Best Practices ```gdscript # preload() for always-needed scenes (compile-time) const BulletScene = preload("res://scenes/entities/bullet.tscn") func spawn_bullet(): var bullet = BulletScene.instantiate() add_child(bullet) # load() for conditional scenes (runtime) func load_level(level_name: String): var path = "res://levels/" + level_name + ".tscn" var scene = load(path) # Cached by Godot internally if scene: current_level = scene.instantiate() add_child(current_level) ``` ### Level of Detail (LOD) Use LOD to reduce rendering cost for distant objects. ```gdscript class_name LODObject extends Node3D @export var lod_distances: PackedFloat32Array = [10.0, 30.0, 60.0] @onready var lod_nodes = [$HighDetail, $MediumDetail, $LowDetail] func _process(): var dist = global_position.distance_to(get_viewport().get_camera3D().global_position) for i in range(lod_nodes.size()): lod_nodes[i].visible = dist < lod_distances[i] ``` ### Instanced Rendering Use `MultiMeshInstance2D/3D` for many similar objects. ```gdscript # Draw 1000 identical trees with a single draw call var mesh = preload("res://models/tree.tscn") var multi_mesh = MultiMesh.new() multi_mesh.mesh = mesh.resource multi_mesh.transform_format = MultiMesh.TRANSFORM_3D multi_mesh.instance_count = 1000 for i in range(1000): var x = randf() * 200 - 100 var z = randf() * 200 - 100 multi_mesh.set_instance_transform(i, Transform3D(Basis(), Vector3(x, 0, z))) $MultiMeshInstance3D.multi_mesh = multi_mesh ``` ## Render Passes and Draw Calls ### Minimizing Draw Calls ```gdscript # BAD: Many small sprites (many draw calls) for i in range(100): var sprite = Sprite2D.new() sprite.texture = grass_texture add_child(sprite) # GOOD: Use TileMap or MultiMesh (single draw call) # TileMap handles batching automatically # Or use a custom mesh with instanced rendering ``` ### Render Pass Optimization - **Forward+** renderer: Better for many lights, use for 3D games - **Mobile** renderer: Fewer passes, better for low-end devices - **Stencil shadows**: Use shadow maps instead of volumetric shadows when possible - **LightmapGI**: Bake static lighting to avoid real-time shadows ```gdscript # Set renderer in Project Settings > Rendering > Renderer # Forward Plus: Better quality, higher cost # Mobile: Optimized for low-end hardware ``` ## Mobile-Specific Optimizations ### Texture Compression ```gdscript # Project Settings > Export > Android/iOS # Set texture format: # - ASTC: Best quality/size ratio (modern devices) # - ETC2: Fallback for older Android devices # - DXT: Desktop only # Use compressed textures in the editor: # Texture > Compressed = True ``` ### Mobile Rendering Tips - Use **Mobile renderer** instead of Forward+ for Android/iOS - Limit active lights per object (use light masks) - Bake static lighting with LightmapGI - Use smaller shadow map sizes (512x512 or 256x256) - Disable post-processing effects when not needed - Use `CanvasGroup` to batch UI rendering - Set `texture_filter` to `TEXTURE_FILTER_NEAREST` for pixel art ### CPU Optimization for Mobile - Avoid `_get_node()` in _process — use `@onready` instead - Minimize `_instantiate()` calls — use object pooling - Use `NavigationServer3D` for pathfinding (avoids scene tree overhead) - Batch physics queries instead of individual raycasts - Use `PhysicsServer3D` directly for bulk operations ## Godot 4.7 Performance Features ### Perfetto Tracing for Android (Godot 4.7) Perfetto provides detailed profiling data for Android builds. ```gdscript # Enable in Project Settings > Debug # Perfetto tracing captures: # - Frame timing breakdown # - GC pauses # - Thread scheduling # - GPU command queue # After export, run: # godot --perfetto-trace output.perfetto # Then open in https://ui.perfetto.dev/ ``` ### Nearest-Neighbor Viewport Scaling (Godot 4.7) Pixel-perfect scaling for retro-style games. ```gdscript # Project Settings > Display > Window > Stretch Mode stretch_mode = CanvasGroup.STRETCH_MODE_SCALE stretch_aspect = CanvasGroup.STRETCH_ASPECT_KEEP # Godot 4.7: Use nearest-neighbor scaling for crisp pixel art texture_filter = TextureFilter.NEAREST # Calculate integer scale factor func get_viewport_scale() -> int: var target_width = 320 # Original game resolution return max(1, int(DisplayServer.window_get_size().x / target_width)) ``` ## Performance Checklist ### Before Release - [ ] Profile the release build (not debug) - [ ] Check FPS stays above 30 (60 ideal) on target hardware - [ ] Verify memory usage stays within device limits - [ ] Test on lowest-spec target device - [ ] Check for GC spikes (>1ms pauses) - [ ] Verify texture compression is enabled - [ ] Reduce draw calls with instancing - [ ] Use lightmaps for static scenes - [ ] Implement object pooling for frequently spawned entities ### Runtime Monitoring ```gdscript # Add to debug HUD class_name PerfHUD extends CanvasLayer @onready var fps_label = $FPSLabel @onready var mem_label = $MemLabel func _process(delta): fps_label.text = "FPS: " + str(Performance.get_monitor(Performance.TIME_FPS)) mem_label.text = "Mem: " + str(Performance.get_monitor(Performance.MEMORY_STATIC) / 1024 / 1024) + " MB" ``` ## MCP Bridge Tools (Optional — Live Editor Integration) When the MCP bridge is running (Phase 2), these tools enhance performance debugging: - **`profiler_start`** — Start a profiling session from the editor - **`profiler_stop`** — Stop and display results - **`memory_stats`** — View current memory allocation breakdown - **`render_stats`** — Query draw calls, triangles, and active textures - **`perfetto_export`** — Export Perfetto trace for Android debugging > **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge. ## References - [Profiler Documentation](references/profiler-guide.md) - [Memory Management Patterns](references/memory-patterns.md) - [Mobile Optimization Checklist](references/mobile-checklist.md)