Overview
Tested with: Godot 4.3+
Have you ever experienced stuttering during scene transitions or wanted to display a loading screen? In Godot, choosing the right resource loading method can dramatically improve perceived performance.
Godot's resource loading comes in tiers. "Pre-load small, frequently used resources ahead of time." "Load large resources only when needed." "Load massive resources in the background while showing a loading screen." Mastering this hierarchy is key to a smooth gameplay experience.
This article covers the fundamental differences between preload() and load(), asynchronous loading with ResourceLoader, loading screen implementation, and memory management tips.
preload() vs. load()
Godot offers two basic functions for loading resources. They differ in loading timing and path specification flexibility, so it's important to choose based on your use case.
preload() -- Script Load-Time Loading
For resources you use almost every frame -- like bullets and sound effects -- preload() is the way to go. Since the resource is already in memory when the script is loaded, there's zero delay at call time.
You may see this described as "compile-time loading," but GDScript is an interpreted language, so the resource is technically loaded when the script is parsed (parse time). There's no practical difference, but knowing this helps you understand the mechanism correctly.
# Available immediately when the script loads (path must be a constant)
const BulletScene = preload("res://scenes/bullet.tscn")
const HitSound = preload("res://audio/hit.wav")
func shoot():
var bullet = BulletScene.instantiate()
add_child(bullet)
- Path must be a string literal (variables are not allowed)
- Loaded all at once when the script is parsed
- Ideal for small or frequently used assets
load() -- Runtime Loading
On the other hand, for resources that depend on conditions -- like a player's chosen weapon or enemies scaled to difficulty -- use load(). Its biggest strength is the ability to build paths from variables.
# Load dynamically at runtime (variable paths are allowed)
func load_weapon(weapon_name: String):
var scene_path = "res://weapons/%s.tscn" % weapon_name
var weapon_scene = load(scene_path)
return weapon_scene.instantiate()
# Switch based on conditions
func get_enemy_scene(difficulty: int) -> PackedScene:
if difficulty >= 3:
return load("res://enemies/boss.tscn")
return load("res://enemies/normal.tscn")
- Paths can be built dynamically
- Reads from disk on first call (returns from cache on subsequent calls)
- Suited for large resources or conditional loading
Quick Reference
| Aspect | preload() | load() |
|---|---|---|
| Load timing | Script load time (parse time) | Runtime |
| Path specification | Literals only | Variables allowed |
| Blocking | During scene load | At call site |
| Recommended for | Bullets, SFX, UI elements | Stage data, selectable assets |
| Caching | Automatic | Automatic (disk I/O on first call only) |
tips: Using too many
preload()calls slows down the initial scene load. Large textures and 3D models have the biggest impact, so consider switching heavy resources toload()or background loading. It's the total size that matters, not the number of preloads.
Background Loading with ResourceLoader
Now that you know how to choose between preload() and load(), let's take it a step further. When loading large scenes or stage data, load() blocks the main thread and causes freezing. For heavy resources -- like RPG dungeon transitions or open-world chunk loading -- the ResourceLoader async API really shines. You can load in the background while keeping the game running.
Basic Async Loading Flow
Async loading is implemented in three steps: "request → monitor progress → retrieve." Unlike regular load() which freezes the game until done, this approach lets you monitor progress while keeping the game running.
# 1. Start the request (loading begins on a separate thread)
ResourceLoader.load_threaded_request("res://levels/stage_2.tscn")
# 2. Check progress (call every frame)
func _process(_delta):
var progress = [] # Passed as an array (Godot convention)
var status = ResourceLoader.load_threaded_get_status(
"res://levels/stage_2.tscn", progress
)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
# progress[0] contains a value from 0.0 to 1.0
print("Loading: %d%%" % int(progress[0] * 100))
ResourceLoader.THREAD_LOAD_LOADED:
# 3. Loading complete -> retrieve the resource
var scene = ResourceLoader.load_threaded_get(
"res://levels/stage_2.tscn"
)
_on_load_complete(scene)
ResourceLoader.THREAD_LOAD_FAILED:
printerr("Load failed!")
ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
printerr("Invalid path or not requested!")
tips: The
progressparameter is passed as an array because GDScript functions can't return multiple values directly. You pass an empty array and the engine populatesprogress[0]with the current progress value -- this is a common Godot pattern for "out parameters."
The Three API Methods
| Method | Role |
|---|---|
load_threaded_request(path) | Start async loading |
load_threaded_get_status(path, progress) | Get progress (0.0 to 1.0) |
load_threaded_get(path) | Retrieve the resource after completion |
load_threaded_get_status() returns one of four statuses.
| Status | Meaning |
|---|---|
THREAD_LOAD_IN_PROGRESS | Loading in progress |
THREAD_LOAD_LOADED | Loading complete |
THREAD_LOAD_FAILED | Loading failed |
THREAD_LOAD_INVALID_RESOURCE | Invalid path or request not made |
tips: Passing a type hint like
"PackedScene"as the second argument toload_threaded_request()enables type-checked loading.
Speeding Up with use_sub_threads
Setting the third argument use_sub_threads to true in load_threaded_request() enables parallel loading of sub-resources (textures, meshes, etc.). For scenes with many sub-resources, this can significantly reduce load times.
# Enable parallel sub-resource loading (default is false)
ResourceLoader.load_threaded_request(
"res://levels/stage_2.tscn",
"", # Type hint (empty string for auto-detection)
true # use_sub_threads = true
)
tips: Calling
load_threaded_request()twice with the same path will produce an error. If multiple parts of your code might trigger loading, check the status withload_threaded_get_status()first before making the request.
Implementing a Loading Screen
Now that you understand the async loading API, let's build a UI to show the player what's happening. Here's a practical loading screen implementation using background loading. By combining a progress bar with percentage display, you can clearly communicate wait times to users.
# LoadingScreen.gd
extends CanvasLayer
@onready var progress_bar: ProgressBar = $ProgressBar
@onready var label: Label = $Label
var target_scene_path: String = ""
func load_scene(scene_path: String):
target_scene_path = scene_path
show()
# Start async loading
var err = ResourceLoader.load_threaded_request(scene_path)
if err != OK:
printerr("Failed to start loading: %s" % scene_path)
return
set_process(true)
func _process(_delta):
if target_scene_path.is_empty():
return
var progress = []
var status = ResourceLoader.load_threaded_get_status(
target_scene_path, progress
)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
progress_bar.value = progress[0] * 100
label.text = "Loading... %d%%" % int(progress[0] * 100)
ResourceLoader.THREAD_LOAD_LOADED:
progress_bar.value = 100
var scene = ResourceLoader.load_threaded_get(target_scene_path)
get_tree().change_scene_to_packed(scene)
target_scene_path = ""
set_process(false)
hide()
ResourceLoader.THREAD_LOAD_FAILED:
label.text = "Load failed."
set_process(false)
Usage (registering as an Autoload is recommended):
# Callable from anywhere
LoadingScreen.load_scene("res://levels/stage_2.tscn")
tips: This LoadingScreen must be registered as an Autoload (singleton). Without Autoload,
change_scene_to_packed()replaces the entire scene tree, which would destroy the LoadingScreen itself. Register it under "Project Settings → Autoload" so it persists across scene changes.
Memory Management and Caching Strategies
So far we've focused on how to load resources. But how to manage them afterward is just as important. Managing resource loading is only half the story -- freeing unneeded resources is equally important. In large games, failure to properly free unused resources per stage can lead to memory shortages.
Godot's Resource Cache
First, let's understand Godot's built-in caching mechanism. Godot caches resources loaded via load() by their path. Calling load() again with the same path returns the cached in-memory version without disk I/O.
# Second call onward returns from cache (no disk I/O)
var tex_a = load("res://textures/player.png")
var tex_b = load("res://textures/player.png")
# tex_a == tex_b (same instance)
Holding References with WeakRef
When you want to cache large resources but allow automatic cleanup when they're no longer needed, use WeakRef.
Godot's resources (Resource class) are managed through reference counting. Each resource tracks how many variables reference it, and the resource is freed immediately when that count drops to zero. This is different from tracing garbage collectors in Java or C# -- Godot's approach is deterministic and immediate.
Regular variable references increment the count, preventing the resource from being freed. WeakRef, on the other hand, does not increment the reference count. This means the resource can be freed when no other "strong" references exist, creating a "weak reference" pattern.
var _cache: Dictionary = {}
func get_resource(path: String) -> Resource:
# Return from cache if available
if _cache.has(path):
var weak: WeakRef = _cache[path]
var res = weak.get_ref()
if res:
return res
# Load and cache if not found
var res = load(path)
_cache[path] = weakref(res)
return res
func clear_cache():
_cache.clear()
# Resources held only by WeakRef are freed when reference count hits zero
When get_ref() returns null, that resource has already been freed from memory. In that case, load() re-reads it from disk, so for frequently accessed resources a regular variable cache is more efficient. WeakRef caching is best for resources that "might be needed again, but saving memory is the priority."
Memory Management Best Practices
With the individual techniques covered, let's outline a project-level strategy. Following these guidelines for resource management design achieves a good balance between memory efficiency and performance.
| Strategy | Specifics |
|---|---|
| Manage per stage | Discard unneeded resource references on stage transitions |
| Minimize preload | Only preload assets used across all scenes; use load() for stage-specific ones |
| Soft cache with WeakRef | For resources that might be reused but don't need to stay in memory |
| Async load large resources | Use load_threaded_request() for texture atlases and 3D models |
Summary
- preload() loads at script parse time; ideal for small, frequently used resources
- load() loads at runtime; supports dynamic paths and conditional loading
- ResourceLoader.load_threaded_request() enables background loading; set
use_sub_threads = truefor even faster loading - load_threaded_get_status() monitors progress for updating loading screen progress bars; handles 4 status types for proper error handling
- Godot resources use reference counting for memory management. WeakRef enables soft caching without incrementing the reference count
- Register loading screens as Autoload to prevent them from being destroyed during scene transitions