How to speed up loading times in your Godot game by using ResourceLoader.load_threaded_request

No one likes staring at a frozen screen while a big scene loads. In many games, switching between menus and gameplay can cause noticeable hitches or stuttering, especially when loading large resources like 3D levels, audio banks, or video files.
Luckily, Godot provides a built-in way to load resources in the background using threads, so your game can keep showing a loading screen (or even play an animation or video) while the heavy lifting happens off the main thread.
In this article, we’ll walk through how to use ResourceLoader.load_threaded_request()
to make smooth loading screens for your game.
The Problem: Blocking Loads
Normally, when you load a scene with ResourceLoader.load("res://scene.tscn")
or load("res://scene.tscn")
, the engine stops everything until that resource is fully loaded. This is fine for tiny scenes, but when the resources get bigger, the main thread stalls — and the player sees a frozen frame.
The Solution: Threaded Loading
Godot’s ResourceLoader
has a set of functions for threaded loading:
Normally, when you load a resource with load()
, the main thread (the one that runs your game loop, animations, and input) stops until the resource is fully ready. That’s why you sometimes see freezes when switching to a heavy scene.
Threaded loading solves this by moving the actual loading work into a background thread.
- The main thread keeps running → your game can still draw a loading screen, play music, or animate a spinner.
- Meanwhile, in the background, Godot is reading and preparing the resource.
- You just check the loading status each frame until it’s done, and then you can safely use the resource.
This way, players see a relatively smooth transition instead of a frozen screen.
Example: A Simple Threaded Scene Loader
Let’s put this into practice. We’ll build:
- A loading screen scene (e.g., with a spinner animation).
- A small helper class that requests threaded loading and emits a signal once the resource is ready.
class_name ThreadedResourceLoader
signal scene_loaded(scene: PackedScene)
var _scene_path: String
var _tween: Tween
func _init(scene_path: String, parent: Node) -> void:
_scene_path = scene_path
# Start threaded load
ResourceLoader.load_threaded_request.call_deferred(scene_path, "PackedScene")
# Use a Tween to check progress every 0.01s
_tween = parent.create_tween().set_loops()
_tween.tween_callback(_check_status).set_delay(0.01)
func _check_status() -> void:
var status := ResourceLoader.load_threaded_get_status(_scene_path)
if status != ResourceLoader.THREAD_LOAD_IN_PROGRESS:
# Stop the tween
_tween.kill()
# Emit the loaded resource
var res = ResourceLoader.load_threaded_get(_scene_path)
scene_loaded.emit(res)
And how do we use the loader?
extends Control
func _ready() -> void:
var loader = ThreadedResourceLoader.new("res://secondary.tscn", self)
var scene: PackedScene = await loader.scene_loaded
if scene:
add_child.call_deferred(scene.instantiate())
else:
push_error("Failed to load scene")
loader = null
Skippable splash screen logic
Sometimes you want a splash/intro you can skip with a key press. In that case, you can still start loading your main scene in the background with load_threaded_request()
, but if the player skips early, you can force-complete the load.
A neat way to do this is to hook into the splash node’s tree_exiting
signal (emitted when you close the splash on user input), and grab the resource right there:
extends Control
var _scene_loaded := false
func _ready() -> void:
var splash = preload("res://splash.tscn").instantiate()
add_child.call_deferred(splash)
splash.tree_exited.connect(_on_splash_tree_exited)
var loader = ThreadedResourceLoader.new("res://secondary.tscn", self)
var scene = await loader.scene_loaded
if _scene_loaded:
return
_scene_loaded = true
if scene:
add_child.call_deferred(scene.instantiate())
else:
push_error("Failed to load scene")
func _on_splash_tree_exited() -> void:
if _scene_loaded:
return
# maybe just wait a bit in case the threaded loading hasn't finished yet as that should still be better
await get_tree().create_timer(0.1).timeout
_scene_loaded = true
var scene = ResourceLoader.load("res://secondary.tscn")
print(scene)
add_child.call_deferred(scene.instantiate())