【Godot】Dynamic Loading and Resource Management in Godot (load, preload, Background Loading)

Created: 2026-02-08

Learn how to choose between load() and preload(), implement background loading with ResourceLoader, build loading screens, and apply memory management best practices.

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

Aspectpreload()load()
Load timingScript load time (parse time)Runtime
Path specificationLiterals onlyVariables allowed
BlockingDuring scene loadAt call site
Recommended forBullets, SFX, UI elementsStage data, selectable assets
CachingAutomaticAutomatic (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 to load() 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 progress parameter is passed as an array because GDScript functions can't return multiple values directly. You pass an empty array and the engine populates progress[0] with the current progress value -- this is a common Godot pattern for "out parameters."

The Three API Methods

MethodRole
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.

StatusMeaning
THREAD_LOAD_IN_PROGRESSLoading in progress
THREAD_LOAD_LOADEDLoading complete
THREAD_LOAD_FAILEDLoading failed
THREAD_LOAD_INVALID_RESOURCEInvalid path or request not made

tips: Passing a type hint like "PackedScene" as the second argument to load_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 with load_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.

StrategySpecifics
Manage per stageDiscard unneeded resource references on stage transitions
Minimize preloadOnly preload assets used across all scenes; use load() for stage-specific ones
Soft cache with WeakRefFor resources that might be reused but don't need to stay in memory
Async load large resourcesUse 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 = true for 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

Further Reading