Introduction: Why does the screen freeze even though it is asynchronous?
Addressables Asset System (Addressables) is the de facto standard for asset management in Unity development. One of the major purposes of introducing this system is to "elimate loading screens and avoid small freezes (spikes) by asynchronous loading" using Addressables.LoadAssetAsync.
However, even though you are using the asynchronous API as designed, have you ever been bothered by the phenomenon where the screen freezes for a moment when you start loading assets? If you check with Profiler, you may see that the processing time of the main thread jumps (spikes) in the frame where the load API is called.
Let's use a ``parable'' to solve this mystery.
When you go on a trip, let's say you pack your clothes (assets) tightly into a super-compressed bag (LZMA) and put them into one suitcase. What would you do if you needed a pair of socks while traveling? You can't remove a pair of socks unless you unzip the bag, let air in, and spread out the contents (unzip the entire bag). This is LZMA compression.
On the other hand, suppose you have an organizedcabinet with drawers (LZ4). If you need socks, you can just pull out the "sock drawer (specific chunk)" and get the contents out; you don't even have to touch the other drawers. This is LZ4 compression. Even if Unity tries to do an asynchronous load, opening the entire suitcase (unzipping the entire LZMA) will occupy the main thread for a long time.
1. Root cause: The truth behind "LZMA decompression" that runs behind asynchronous loading
There are two main compression formats for Unity's AssetBundle (the entity of Addressables): LZMA and LZ4. The specifications of these two compression algorithms and their behavior during decompression are the biggest cause of main thread spikes during asynchronous loads.
| Compression format | Compression ratio (data size) | Main thread load during decompression | Asset loading behavior |
|---|---|---|---|
| LZMA | Extremely tall (minimum size) | Very high (causing spikes) | You need to unzip the whole bundle at once |
| LZ4 | Medium (larger than LZMA) | Very low (no spikes) | On-demand decompression of only chunks that contain necessary assets |
LZMA is a method called "stream-based compression" that compresses the entire file as one huge data stream. Therefore, Unity must unzip and unarchive the entire asset bundle even if you only want to load some assets.
The problem is that this LZMA decompression process is done by blocking the main thread instead of the I/O thread (background thread). When you call the asynchronous load API (LoadAssetAsync), Unity internally runs asynchronous processing tasks, but header analysis of file archives, integrity checks of compressed files, and some of the initial processing of serialization decompression are executed synchronously on the main thread. If the size of the asset bundle is large, exceeding 50MB to 100MB, this decompression overhead alone will cause the main thread to completely freeze for 100ms to 500ms (several frames to tens of frames), which will be perceived by the player as a "screen stutter (spike)."
2. Solution approach: Steps to migrate to LZ4 (Chunk-based) compression
The most effective approach to completely eliminate this load spike is to change the compression setting for the Addressables group from "LZMA" to "LZ4". LZ4 uses "chunk-based compression", which divides files into small blocks (chunks) and compresses them individually. When loading an asset, we only decompress the specific chunk in which the asset is stored, minimizing the memory and time required for decompression and completely eliminating main thread spikes.
Step 1: Change settings in the Addressables Groups window
- Open
Window > Asset Management > Addressables > Groupsfrom the Unity editor menu. - Select the group of interest (for example:
Default Local Group) and look at the Inspector window. - Find the Bundle Compression setting in the
Content Packing & Loadingsection. - Change the setting value from LZMA to LZ4 (or
Uncompressed).
When changing to LZ4, the physical file size of the asset bundle will be about 1.2 to 1.5 times larger than LZMA. This may be a concern for mobile games where you want to reduce download capacity (build capacity) to the absolute minimum, but considering the stress (stutteriness) experienced by players when loading, LZ4 should basically be used. There is also a design that uses LZMA only for remote groups for distribution to reduce the download size, and stores and decompresses it locally as an LZ4 cache in the background before loading.
Step 2: Safe implementation of asynchronous loading and cache management
In addition to changing the compression format, the C# code also needs to smartly control asynchronous loading and properly separate the loading and instantiation (creation) loads. Below is an example implementation of a robust manager class to safely load and cache assets asynchronously while preventing spikes.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AddressableAssetLoader : MonoBehaviour
{
// Cache dictionary to prevent double loading of assets
private Dictionary<string, AsyncOperationHandle> _loadedAssets = new Dictionary<string, AsyncOperationHandle>();
/// <summary>
/// Safely load assets asynchronously (does not block the main thread)
/// </summary>
public void LoadAssetSafe<T>(string address, Action<T> onComplete) where T : UnityEngine.Object
{
// Immediate return from cache if already loaded
if (_loadedAssets.TryGetValue(address, out AsyncOperationHandle existingHandle))
{
if (existingHandle.IsDone && existingHandle.Status == AsyncOperationStatus.Succeeded)
{
onComplete?.Invoke(existingHandle.Result as T);
return;
}
}
// Start an asynchronous load request
AsyncOperationHandle<T> handle = Addressables.LoadAssetAsync<T>(address);
_loadedAssets[address] = handle;
handle.Completed += (op) =>
{
if (op.Status == AsyncOperationStatus.Succeeded)
{
// Execute callback on main thread when loading completes
onComplete?.Invoke(op.Result);
}
else
{
Debug.LogError($"[Loader] アセットのロードに失敗しました: {address}");
_loadedAssets.Remove(address);
}
};
}
/// <summary>
/// Asset memory release process
/// </summary>
public void ReleaseAsset(string address)
{
if (_loadedAssets.TryGetValue(address, out AsyncOperationHandle handle))
{
Addressables.Release(handle);
_loadedAssets.Remove(address);
Debug.Log($"[Loader] アセットをアンロードしました: {address}");
}
}
private void OnDestroy()
{
// Safely release all loaded assets at once during scene transition or object destruction
foreach (var handle in _loadedAssets.Values)
{
Addressables.Release(handle);
}
_loadedAssets.Clear();
}
}3. “Road spike prevention checklist” useful in practice
As a countermeasure for processing failures related to asset management, we have summarized important points that should be confirmed before release.
| Check items | Recommended settings/actions | Reason and effect |
|---|---|---|
| Local group compression format | Unified to LZ4 (Chunk-based) | Prevent the main thread from freezing (more than 0.1s) due to decompression processing on the actual machine. |
| Preloading (warming) | Preload in quiet scenes (during transitions, etc.) before battles or heavy production begin | Hides the time required for Instantiate and shader compilation at a time that does not affect gameplay. |
| Shader precompilation | Preload and warm up Shader Variant Collection | Prevents heavy processing slowdowns (stalls) of seconds due to "shader compilation" that occurs after loading assets. |
| Bundle subdivision | Divide groups by function or category (3D characters, SE, UI, etc.) | Prevents unrelated large AssetBundles from sitting in memory, resolves dependency cycles, and speeds up loading and unloading. |
Summary: For comfortable loading
Don't feel safe just because you are using an asynchronous load API; it is important to always be aware of the internal behavior (LZMA vs LZ4, etc.) of ``How assets are decompressed behind the scenes and when they are expanded to memory.''
Especially in environments where thermal throttling (deterioration in performance due to thermal runaway) is prone to occur, such as smartphones and standalone VR, the accumulation of these decompression spikes can directly lead to forced termination of apps, stuttering, and ultimately a decrease in user satisfaction. Build a stress-free and comfortable asset loading environment by leveraging appropriate group settings and asynchronous manager class caching.