agentleFS
vl4dtvl4dt/godot-skills/skills/godot-performance/SKILL.md

godot-performance

Skill8 starsChanged 5 months agoMITView on GitHub

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.

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 5 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use LOD to reduce rendering cost for distant objects.

    line 157
  2. Very specific:

    Use MultiMeshInstance2D/3D for many similar objects.

    line 173
  3. Distinctive:

    Stencil shadows: Use shadow maps instead of volumetric shadows when possible

    line 210
  4. Distinctive:

    Use Mobile renderer instead of Forward+ for Android/iOS

    line 234
  5. Distinctive:

    Use smaller shadow map sizes (512x512 or 256x256)

    line 237
  6. Very specific:

    Use CanvasGroup to batch UI rendering

    line 239
  7. Very specific:

    Avoid _get_node() in _process — use @onready instead

    line 244
  8. Very specific:

    Use NavigationServer3D for pathfinding (avoids scene tree overhead)

    line 246
  9. Very specific:

    Use PhysicsServer3D directly for bulk operations

    line 248
  10. Distinctive:

    When the MCP bridge is running (Phase 2), these tools enhance performance debugging:

    line 315

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.

# 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

# 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.

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.

class_name ObjectPool<T> 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

# 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.

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.

# 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

# 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
# Set renderer in Project Settings > Rendering > Renderer
# Forward Plus: Better quality, higher cost
# Mobile: Optimized for low-end hardware

Mobile-Specific Optimizations

Texture Compression

# 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.

# 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.

# 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

# 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
  • Memory Management Patterns
  • Mobile Optimization Checklist

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.