Preventing Render Freezes in LibGDX with AssetManager
Stop LibGDX render freezes by moving asset loading to a background thread. Learn how to use AssetManager for asynchronous loading, progress tracking, and native memory cleanup.
10 Dec 2025, 00:14 UTC

Loading a texture or sound using a direct constructor blocks the rendering thread. On mobile devices, where the main thread also handles input and UI, a single large atlas can cause a visible hitch or a completely frozen loading screen. The solution is to move asset acquisition off the render thread using the AssetManager to track progress and manage resource lifetimes.
The cost of synchronous I/O
LibGDX typically runs rendering, input, and game logic on a single thread. When you call new Texture(Gdx.files.internal("big.png")), the engine decodes the image data and uploads it to OpenGL immediately. Until this process completes, the frame cannot be presented to the screen. The same blocking behavior applies to Sound, Music, and model data, resulting in stutters that are particularly noticeable on devices with slower storage.
AssetManager solves this by queuing requests and processing them on a background thread. It maintains reference counts for each asset and provides a polling API, allowing the render loop to continue executing—and drawing a loading animation—while the heavy lifting happens in the background.
Asynchronous loading mechanics
Loading is split into two distinct phases. First, manager.load() registers a request and returns immediately without blocking. Second, manager.update() must be called within the render loop; it advances the background work and returns true if the manager's state changed during that frame.
Progress is tracked via manager.getProgress(), which returns a float from 0.0 to 1.0, and completion is verified via manager.isFinished(). It is important to note that only specific built-in types support true asynchronous loading, including Texture, TextureAtlas, Sound, Music, and BitmapFont. If you implement custom loaders, they must be written to avoid blocking the loading thread, or they will still cause hitches during manager.update().
Implementing a responsive loading pattern
The most effective pattern is to instantiate an AssetManager for a specific screen or level, queue the required assets, and gate gameplay access behind the isFinished() check.
// Initialize in a Screen or Game class
AssetManager manager = new AssetManager();
// Queue assets (non-blocking)
manager.load("data/myAtlas.atlas", TextureAtlas.class);
manager.load("effects/jump.wav", Sound.class);
public void render(float delta) {
// Advance the loading process
manager.update();
float progress = manager.getProgress();
// Use progress (0.0 to 1.0) to scale a loading bar UI
if (manager.isFinished()) {
// Safe to retrieve assets now
TextureAtlas atlas = manager.get("data/myAtlas.atlas", TextureAtlas.class);
Sound jump = manager.get("effects/jump.wav", Sound.class);
// Transition to gameplay
}
}
Critical risk: Never call manager.get() before the asset has finished loading, as this will throw an exception. Additionally, AssetManager manages native OpenGL resources that the Java Garbage Collector cannot see. You must call manager.dispose() when the screen or level is unloaded to prevent native memory leaks.
Trade-offs and memory management
While AssetManager provides smoothness, it introduces manual bookkeeping. Because it uses reference counting, an asset remains in memory until all owners call manager.unload(). In memory-constrained environments like Android, keeping too many large assets alive across level transitions can lead to OutOfMemory errors.
To verify the implementation, set up a desktop project and queue a large PNG. If getProgress() increments smoothly over several frames while a UI element moves, the loading is asynchronous. If the UI freezes and then jumps to 100%, the operation is blocking. Finally, calling manager.dispose() should render the retrieved textures as white placeholders or make them disappear, confirming the native handles were released.
Actionable summary
To maintain a high frame rate during asset transitions: queue assets with load(), poll progress with update() in the render loop, retrieve assets only after isFinished() returns true, and always call dispose() to clear native memory.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.