
Godot Performance Optimization
- 198 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-performance-optimization for development tasks
About
godot-performance-optimization: A skill for development. This provides functionality for development workflows.
- godot-performance-optimization
Godot Performance Optimization by the numbers
- 198 all-time installs (skills.sh)
- +13 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,016 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-performance-optimizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 198 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-performance-optimization for development tasks
Files
Performance Optimization
Profiler-driven analysis, object pooling, and visibility culling define optimized game performance.
Available Scripts
worker_thread_pool_manager.gd
Expert logic for offloading heavy computation to Godot 4's WorkerThreadPool for multi-threaded processing.
object_pool_system.gd
Minimal allocation strategy using node visibility and process toggling instead of constant instantiation.
rendering_server_direct.gd
Bypassing the SceneTree logic for massive canvas item rendering directly via the RenderingServer.
low_level_physics_query.gd
High-performance direct physics space state queries, faster than using RayCast nodes for hundreds of checks.
custom_monitor_profiler.gd
Implementation of real-time performance monitoring using Performance.get_monitor() for bottleneck detection.
manual_culling_logic.gd
Disabling off-screen logic manually using VisibilityNotifiers to cull CPU-heavy processing.
shared_resource_strategy.gd
Expert management of Local-to-Scene vs Shared resources to balance memory usage and unique instance states.
texture_array_batching.gd
Reducing draw calls and state changes by utilizing TextureArrays for multi-item shader-based batching.
multimesh_optimizer.gd
Rendering thousands of animated mesh instances via hardware instancing (MultiMeshInstance3D).
navigation_agent_optimization.gd
Staggered path update strategy for massive AI crowds to prevent pathfinding bottlenecks in a single frame.
NEVER Do in Performance Optimization
- NEVER optimize without profiling first — "I think physics is slow" without data? Premature optimization. ALWAYS use Debug → Profiler (F3) to identify actual bottleneck [20].
- NEVER use `print()` in release builds —
print()every frame = file I/O bottleneck + log spam. Use@warning_ignoreor conditionalif OS.is_debug_build():[21]. - NEVER ignore `VisibleOnScreenNotifier2D` for off-screen entities — Enemies processing logic off-screen = wasted CPU. Disable
set_process(false)whenscreen_exited[22]. - NEVER instantiate nodes in hot loops —
for i in 1000: var bullet = Bullet.new()= 1000 allocations. Use object pools, reuse instances [23]. - NEVER use `get_node()` in `_process()` — Calling
get_node("Player")60x/sec = tree traversal spam. Cache in@onready var player := $Player[24]. - NEVER forget to batch draw calls — 1000 unique sprites = 1000 draw calls. Use TextureAtlas (sprite sheets) + MultiMesh for instanced rendering [25].
- NEVER block the main thread for heavy operations — Avoid
OS.delay_msec()or long synchronous data processing. UseWorkerThreadPoolto keep framerates steady. - NEVER use complex collision shapes for physics queries — High-poly convex shapes are expensive to resolve. Prefer simplified primitives (Circle, Rectangle, Box).
- NEVER forget to disconnect local lambda signals — Anonymous lambdas connected to global signals can cause memory leaks if the capturing object is freed.
- NEVER use large textures without VRAM compression — VRAM is limited. Use S3TC/BPTC for desktop (DirectX/Vulkan) and ETC2 for mobile. Note: Disable compression for Pixel Art to avoid artifacts [13].
- NEVER perform tree modifications during physics steps — Adding/removing nodes during
_inter_rayor_physics_processcan lock the physics server. Usecall_deferred. - NEVER skip shader pre-warming in the Compatibility renderer — Unlike Forward+, OpenGL lacks Ubershaders. Pre-instantiate every mesh/VFX in front of the camera for 1 frame behind a loading screen to avoid hitches [21].
---
Debug → Profiler (F3)
Tabs:
- Time: Function call times
- Memory: RAM usage
- Network: RPCs, bandwidth
- Physics: Collision checks
Common Optimizations
Object Pooling
var bullet_pool: Array[Node] = []
func get_bullet() -> Node:
if bullet_pool.is_empty():
return Bullet.new()
return bullet_pool.pop_back()
func return_bullet(bullet: Node) -> void:
bullet.hide()
bullet_pool.append(bullet)Visibility Notifier
# Add VisibleOnScreenNotifier2D
# Disable processing when off-screen
func _on_screen_exited() -> void:
set_process(false)
func _on_screen_entered() -> void:
set_process(true)AStar-Throttler (Pathfinding Budget)
Spreading pathfinding costs over multiple frames to prevent frame-time spikes.
- Implementation:
var _query_queue: Array[Callable] = []
const TIME_BUDGET_USEC := 1000 # 1ms budget
func _process(_delta):
var start_time := Time.get_ticks_usec()
while not _query_queue.is_empty() and (Time.get_ticks_usec() - start_time) < TIME_BUDGET_USEC:
var query = _query_queue.pop_front()
query.call()Shader-Preloading (Zero-Hitch Strategy)
- Forward+ / Mobile: Godot 4.4+ uses Ubershaders for automatic precompilation. Ensure scenes are instantiated at least once (even if invisible) at load-time to trigger pipeline detection [19].
- Compatibility (OpenGL): Place a hidden
Camera3Dlooking at a small area containing every unique mesh and material in your project for 1 frame during the loading screen [20].
VRAM Compression Guide
- S3TC (Desktop): Best for high-quality textures on Windows/Linux/macOS.
- BPTC (Desktop): Superior quality for HDR and normal maps; slightly higher VRAM usage.
- ETC2 (Mobile): The standard for Android/iOS; ensure textures are opaque where possible for maximum compatibility [13].
Reference
Related
- Master Skill: godot-master
# custom_monitor_profiler.gd
# Monitoring performance bottlenecks in real-time
extends Label
# EXPERT NOTE: Use Performance.get_monitor to create
# custom debug overlays that catch regression during play.
func _process(_delta):
text = "FPS: %d\n" % Engine.get_frames_per_second()
text += "Draw Calls: %d\n" % Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
text += "Static Memory: %s\n" % String.humanize_size(Performance.get_monitor(Performance.MEMORY_STATIC))
text += "Objects: %d\n" % Performance.get_monitor(Performance.OBJECT_COUNT)
# skills/performance-optimization/scripts/custom_performance_monitor.gd
extends Node
## Custom Performance Monitor Expert Pattern
## Adds game-specific metrics to the Godot Debugger > Monitors tab.
class_name CustomPerformanceMonitor
enum MonitorType {
ENEMY_COUNT,
ACTIVE_PROJECTILES,
PATHFINDING_TIME_MS,
CHUNK_LOAD_TIME_MS
}
# Map enum to readable names
var _monitor_names = {
MonitorType.ENEMY_COUNT: "Game/Enemy Count",
MonitorType.ACTIVE_PROJECTILES: "Game/Active Projectiles",
MonitorType.PATHFINDING_TIME_MS: "Game/Pathfinding (ms)",
MonitorType.CHUNK_LOAD_TIME_MS: "Game/Chunk Load (ms)"
}
var _monitor_values = {}
func _ready() -> void:
# Only run in debug builds
if not OS.is_debug_build():
queue_free()
return
# Register monitors
for type in _monitor_names:
var path = _monitor_names[type]
if not Performance.has_custom_monitor(path):
Performance.add_custom_monitor(path, _get_monitor_value.bind(type))
_monitor_values[type] = 0.0
func update_monitor(type: MonitorType, value: float) -> void:
if OS.is_debug_build():
_monitor_values[type] = value
func increment_monitor(type: MonitorType, amount: float = 1.0) -> void:
if OS.is_debug_build():
if type in _monitor_values:
_monitor_values[type] += amount
else:
_monitor_values[type] = amount
func _get_monitor_value(type: MonitorType) -> float:
return _monitor_values.get(type, 0.0)
## EXPERT USAGE:
## CustomPerformanceMonitor.update_monitor(CustomPerformanceMonitor.MonitorType.ENEMY_COUNT, enemies.size())
# low_level_physics_query.gd
# Direct PhysicsServer queries for high performance
extends Node3D
# EXPERT NOTE: Direct space queries via DirectSpaceState
# are faster than RayCast nodes when doing hundreds of checks
# per frame (e.g. for AI vision or custom particles).
func _physics_process(_delta):
var space_state = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(global_position, global_position + Vector3.FORWARD * 10)
query.exclude = [get_rid()]
var result = space_state.intersect_ray(query)
if result:
# Handle hit
pass
# manual_culling_logic.gd
# Disabling off-screen logic manually
extends VisibleOnScreenNotifier2D
# EXPERT NOTE: VisibilityNotifiers are the most efficient
# way to cull heavy _process or _physics_process logic
# when nodes are not visible to the camera.
func _ready():
screen_entered.connect(_on_screen_entered)
screen_exited.connect(_on_screen_exited)
func _on_screen_entered():
set_process(true)
set_physics_process(true)
func _on_screen_exited():
set_process(false)
set_physics_process(false)
# skills/performance-optimization/code/multimesh_foliage_manager.gd
extends MultiMeshInstance3D
## MultiMesh Foliage Manager Expert Pattern
## Efficiently renders 10,000s of objects in 1 draw call.
@export var instance_count: int = 10000
@export var area_size: float = 50.0
func _ready() -> void:
# 1. MultiMesh Initialization
# Expert logic: Use 'MultiMeshInterface' to batch geometry.
multimesh = MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_3D
multimesh.instance_count = instance_count
multimesh.mesh = _get_standard_grass_mesh()
_populate_instances()
func _populate_instances() -> void:
# 2. Bulk Transform Assignment
# Professional games set all transforms at once to minimize state changes.
for i in instance_count:
var pos = Vector3(
randf_range(-area_size, area_size),
0,
randf_range(-area_size, area_size)
)
var basis = Basis().rotated(Vector3.UP, randf() * PI)
multimesh.set_instance_transform(i, Transform3D(basis, pos))
func _get_standard_grass_mesh() -> Mesh:
# Placeholder for a simple QuadMesh with a grass texture
return QuadMesh.new()
## EXPERT NOTE:
## Use 'Draw Call Batching': MultiMesh reduces 10,000 individual nodes
## into a SINGLE draw call, drastically reducing CPU/GPU overhead.
## For 'performance-optimization', implement 'VisibilityNotifier' to
## hide the entire MultiMesh node when off-screen.
## NEVER call 'get_node()' or 'find_child()' in '_process'; always
## cache references in '@onready' or unique instance IDs.
# multimesh_optimizer.gd
# Rendering thousands of instances with MultiMeshInstance
extends MultiMeshInstance3D
# EXPERT NOTE: MultiMesh uses hardware instancing. It is
# significantly faster than spawning thousands of
# individual nodes for grass, debris, or particles.
func setup(count: int):
multimesh.instance_count = count
for i in range(count):
var trans = Transform3D(Basis(), Vector3(randf(), 0, randf()) * 10)
multimesh.set_instance_transform(i, trans)
multimesh.set_instance_color(i, Color(randf(), randf(), randf()))
# navigation_agent_optimization.gd
# Staggering paths for massive AI crowds
extends NavigationAgent3D
# EXPERT NOTE: Recalculating hundreds of paths per frame
# kills performance. Stagger updates using a global counter.
static var global_update_tick: int = 0
func _process(_delta):
global_update_tick += 1
# Only update path every 10 frames, staggered by this agent's ID
if (global_update_tick + get_instance_id()) % 10 == 0:
# target_position = ...
pass
# object_pool_system.gd
# Minimal allocation strategy for high-frequency objects
extends Node
# EXPERT NOTE: Instantiating nodes is expensive. Pooling
# avoids the hit by toggling visibility and process mode
# instead of calling .instantiate() and .queue_free().
@export var pool_size: int = 100
@export var bullet_scene: PackedScene
var pool: Array[Node] = []
func _ready():
for i in pool_size:
var node = bullet_scene.instantiate()
_deactivate_node(node)
add_child(node)
pool.append(node)
func spawn():
for node in pool:
if not node.visible:
_activate_node(node)
return node
return null
func _activate_node(node):
node.visible = true
node.set_process(true)
node.set_physics_process(true)
func _deactivate_node(node):
node.visible = false
node.set_process(false)
node.set_physics_process(false)
# rendering_server_direct.gd
# Bypassing the Node tree for massive item rendering
extends Node2D
# EXPERT NOTE: The RenderingServer allows you to draw
# millions of items by avoiding the overhead of the Node
# hierarchy. Each canvas item is just an RID in the server.
var items: Array[RID] = []
func _ready():
var canvas = get_canvas_item()
for i in range(1000):
var item = RenderingServer.canvas_item_create()
RenderingServer.canvas_item_set_parent(item, canvas)
RenderingServer.canvas_item_add_rect(item, Rect2(i, i, 10, 10), Color.RED)
items.append(item)
func _exit_tree():
for item in items:
RenderingServer.free_rid(item)
# shared_resource_strategy.gd
# Using Local-To-Scene vs Shared resources
extends Sprite2D
# EXPERT NOTE: By default, Resources are shared. If you change
# a property in one instance, it changes for all. Toggle
# "Local to Scene" for unique instances or use .duplicate()
# for performance-balanced unique states.
@export var data: Resource
func _ready():
# If we need a unique instance for this node:
if not data.resource_local_to_scene:
data = data.duplicate()
# texture_array_batching.gd
# Reducing state changes with TextureArrays
extends Node
# EXPERT NOTE: Switching textures is a slow "state change"
# for GPUs. Using Texture2DArray or Atlases allows
# the RenderingServer to batch multiple draw calls into one.
@export var tex_array: Texture2DArray
func apply_material_index(sprite: Sprite2D, index: int):
sprite.material.set_shader_parameter("layer_index", index)
# Shader then pulls from tex_array[index]
pass
# worker_thread_pool_manager.gd
# Offloading heavy computation to worker threads
extends Node
# EXPERT NOTE: WorkerThreadPool is superior to Thread.new() in
# Godot 4 as it reuses a pool of system threads, avoiding
# the overhead of spawning new OS threads intermittently.
func process_massive_dataset(data: Array):
var task_id = WorkerThreadPool.add_task(_do_heavy_work.bind(data))
# task_id can be used to check completion or wait
# WorkerThreadPool.is_task_completed(task_id)
func _do_heavy_work(data: Array):
for i in data:
# Process locally without touching the SceneTree directly
pass