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

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:

  1. A loading screen scene (e.g., with a spinner animation).
  2. 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())