Keeping the game smooth: dissecting Godot's built-in profiler and performance monitor, optimizing draw calls and batching, tuning physics that's too expensive, managing memory and FPS budget, and optimization patterns at export time including export templates and platform-specific settings.

In episode 13, you made a multiplayer game — and like any online game, it's prone to lag and frame drops. Episode 14 teaches how to keep the game smooth: performance & profiling. You'll learn to measure before optimizing, because optimization without data is just guesswork.
This episode's roadmap: get to know the built-in profiler and performance monitor, understand draw calls and batching in rendering, optimize physics that's often the culprit, manage memory and FPS budget, then finish with optimization at export time — including export templates and platform-specific settings.
The golden rule of performance: don't optimize before measuring. Godot provides two main tools. The first is the Profiler (Debug menu → Profiler): it records the time every system spends — script, rendering, physics, audio — along with the number of calls per frame. After the game runs for a few seconds, you can read which frames are the heaviest and which system causes it.
The second is the Performance Monitor (Debug → Monitor): real-time graphs of FPS, draw calls, active node count, memory, and more. From here you can see trends: do draw calls spike when many enemies appear? Does memory keep climbing without ever going down? Those two symptoms already give big clues.
The profiler can also be accessed from code for automated telemetry:
func _process(_delta: float) -> void:
var fps: float = Engine.get_frames_per_second()
var physics_time: float = Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS)
if fps < 30.0:
print("FPS menurun: ", fps, " physics: ", physics_time)Build a simple habit: before changing anything, record the FPS and physics time. After the change, compare. If there's no difference, revert — that means your optimization only added complexity without results.
One main cause of a heavy-feeling game is too many draw calls — commands sent by the CPU to the GPU to draw one object. Every sprite, every particle effect, every UI element adds one or more draw calls. The GPU can handle millions of vertices, but the CPU is limited in sending commands, so this is usually where the bottleneck happens.
The key to reducing draw calls is batching: combining many small objects into one draw call. In 2D, automatic batching works when sprites use the same texture and the same material. That's why the most effective pattern is merging many small objects into one texture atlas (one large image containing all sprites), so Godot can draw them all at once.
For objects that move on their own like bullets or particles, limit their numbers: 20 clearly visible bullets beat 200 flooding the screen. Use a pool (a collection of reused objects) instead of constantly creating and destroying nodes:
func tembak(posisi: Vector2) -> void:
for peluru in pool:
if not peluru.visible:
peluru.global_position = posisi
peluru.visible = true
returnPhysics is the second system that most often causes strain. Three high-impact tunings:
CollisionShape2D to objects that don't need collisions, like particle dust.physics/ticks_per_second in Project Settings — half the ticks means half the CPU work for physics.Also watch out for body_entered and body_exited signals, which can flood when many objects touch. Disable monitoring on Area2D nodes that don't need active detection, and consider using set_deferred to change physics properties so they don't happen mid-tick.
Two metrics you must monitor: memory and FPS. Memory that keeps climbing indicates a leak — usually nodes or textures never freed. Always remember Godot's basic patterns: nodes added must be removed, textures loaded must be released. Useful tools are get_node_count() and the ResourceMonitor (Debug → Monitors → Memory).
Unstable FPS often isn't a GPU problem but spikes: one very heavy frame among light ones. Common causes are load() loading a big scene or texture mid-game, or a poorly handled change_scene. The solution: load scenes and assets asynchronously, and show a loading screen while waiting:
func muat_level(level: String) -> void:
ResourceLoader.load_threaded_request(level)
var state := ResourceLoader.load_threaded_get_status(level)
if state == ResourceLoader.THREAD_LOAD_LOADED:
var scene: PackedScene = ResourceLoader.load_threaded_get(level)
get_tree().change_scene_to_packed(scene)Also set a realistic target FPS. Simple 2D games often run at 120 FPS without issue, but if the target device is weak, set Engine.max_fps or application/run/max_fps to maintain consistency and battery life.
Warning
Don't optimize physics or rendering before looking at the profiler. Godot is often already optimal for your game's size, and premature optimization adds bugs. The correct order: measure, find the biggest bottleneck, fix one at a time, then measure again.
Optimization doesn't stop in the editor — export settings also determine performance. In the Export dialog, some high-impact choices:
--headless or --quit-after in CI to make sure the game doesn't error without a display.The habit with the most visible impact on exports: check what gets packaged. Unused scenes or assets increase file size and load time. Use the Resource Manager to clean up unreferenced files before exporting.
When exporting, Godot needs export templates — runtime versions of Godot for the target platform. Install them via the Editor → Manage Export Templates menu, then pick the template in the Export dialog. There are two kinds: debug (for development, bigger and slower) and release (for distribution, smaller and faster). Always export with the release template for end users.
Every platform has specific settings: on Android you set min_sdk and targets; on iOS bitcode and signing; on Windows icons and a clean application; on Linux the AppImage or tar format. The most important thing: test the build early, not at the end — platform problems (icons, permissions, paths) are far cheaper to fix sooner.
Info
Save export presets as .preset files inside the project so they can be version-controlled. That way the whole team exports with the same settings, and your CI can export from a consistent preset.
Performance is a discipline of measuring, not guessing: the built-in profiler and performance monitor show where time really goes, batching and texture atlases reduce draw calls, physics is tuned by limiting bodies and adjusting the tick, memory and FPS are watched with async loading and a target FPS, and at export time you configure texture compression, culling, and use the release export template.
The key takeaways:
ticks_per_second.In episode 15, after the game is fast and smooth, it's your workflow's turn to be optimized: custom tools & editor plugins — creating EditorPlugins, @tool scripts, custom inspectors, and asset pipeline automation. Imagine if the editor could work for you, not the other way around.