One of the most critical and frequent issues encountered when running Unity games on the WebGL platform is an abrupt crash due to "Out of Memory (OOM)". When this occurs, the browser's developer console (F12) displays errors such as Cannot enlarge memory arrays or Out of memory, and the game screen freezes or turns into a browser-native crash screen. In this article, we will explain how the browser's virtual memory limits work with Unity, how to adjust the WebAssembly heap size, and step-by-step techniques to optimize assets to prevent crashes.
1. The Root Cause: WebAssembly (Wasm) Memory Layout and Browser Sandbox Limits
Memory management in WebGL is fundamentally different from native desktop, console, or mobile builds.
A Unity WebGL build translates C# code into C++ via IL2CPP, which is then compiled into WebAssembly (Wasm). Because Wasm runs inside a secure, sandboxed browser environment, it cannot directly access the host system's physical RAM. Instead, it operates within a contiguous block of virtual memory allocated by the browser, known as the Wasm Heap.
Out of Memory crashes occur under three main conditions:
- Exceeding the WebGL Memory Size: When the total memory demanded by active assets (textures, 3D models, audio, etc.) exceeds the initial heap size allocated in Unity's Player Settings (typically 256MB or 512MB by default).
- Browser Tab Memory Caps: Browsers (Chrome, Firefox, Safari) and mobile operating systems enforce strict memory limits per tab. Even if memory growth is allowed, exceeding these system limits causes the browser process to terminate the tab.
- Memory Fragmentation: Frequently loading and unloading assets partitions the Wasm heap into tiny, non-contiguous free blocks. Even if total free memory is high, Unity may crash because it cannot find a single contiguous block of memory large enough to fit a new asset (e.g., a high-resolution texture or a scene AssetBundle).
2. Solution 1: Tuning WebGL Memory Size (Heap Size)
First, optimize the initial WebAssembly heap allocation size to match your game's memory characteristics.
- In the Unity Editor, navigate to Edit > Project Settings > Player.
- Select the WebGL tab and expand the Resolution and Presentation section.
- Locate the WebGL Memory Size field (labeled "Memory Size" in older Unity versions).
This setting determines the contiguous memory pool the WebAssembly instance reserves upon startup.
| Memory Value | Pros | Cons / Risks |
|---|---|---|
| 256MB – 512MB | Highly compatible. Loads reliably on old PCs and mobile browsers. | Easily triggers OOM when loading moderate-to-large scenes. |
| 1024MB (1GB) | Provides a healthy pool suitable for most standard 3D web games. Recommended sweet spot. | Can fail on lower-end mobile devices (like older iPhones using Safari) due to device-specific tab limits. |
| 2048MB (2GB) or more | Allows caching large amounts of assets. | High risk. The browser may fail to find a contiguous 2GB virtual memory slot on startup, causing immediate crash before loading. Highly discouraged. |
Unity enables memory growth by default in recent versions, allowing the WebAssembly heap to expand dynamically. However, whenever the heap resizes, the browser must reallocate and copy the entire heap, resulting in a noticeable performance stutter (freeze) for a few seconds. To ensure a smooth user experience, configure the initial size slightly above your game's peak memory requirement, leaving auto-growth only as a safety net.
3. Solution 2: Streaming Assets and Manual Memory Release
Packaging all assets inside the primary WebGL build forces the entire project into the Wasm heap at startup. Instead, load assets dynamically over the network and purge them from memory when they are no longer in use.
Dynamic Loading via Addressable Assets
Keep the main Wasm binary small by uploading heavy assets (such as character models and environment meshes) to a remote server as AssetBundles. Load them asynchronously only when required.
Here is an example C# script showing how to load and cleanly unload an asset to reclaim heap memory:
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AssetMemoryManager : MonoBehaviour
{
[SerializeField] private string assetAddress = "MyLargeHeavyCharacter";
private GameObject spawnedObject;
private AsyncOperationHandle<GameObject> loadHandle;
public void LoadAsset()
{
// Load asset asynchronously
loadHandle = Addressables.LoadAssetAsync<GameObject>(assetAddress);
loadHandle.Completed += OnLoadCompleted;
}
private void OnLoadCompleted(AsyncOperationHandle<GameObject> handle)
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
spawnedObject = Instantiate(handle.Result);
}
else
{
Debug.LogError($"Failed to load asset: {assetAddress}");
}
}
// Call this to clean up the asset and free memory
public void ReleaseAsset()
{
if (spawnedObject != null)
{
Destroy(spawnedObject);
spawnedObject = null;
}
// Release the Addressables handle to return memory to the heap
if (loadHandle.IsValid())
{
Addressables.Release(loadHandle);
Debug.Log("Asset memory released from Wasm Heap.");
}
// Request Unity to garbage collect unreferenced asset data
Resources.UnloadUnusedAssets();
}
}
4. Solution 3: Optimizing Textures and Audio Compression
Because WebGL runs in browsers where hardware-specific compressed formats (like ASTC or BC7) might not be universally supported, Unity often falls back to uncompressed RGBA 32-bit textures in GPU memory, bloating memory consumption.
- Apply Crunch Compression: Enable Use Crunch Compression in the Inspector for your UI and 3D textures. This significantly reduces both the build bundle download size and the runtime memory footprint.
- Stream Audio Clips: Setting background music (BGM) to "Decompress On Load" forces Unity to decode large uncompressed audio buffers directly into the Wasm heap. Select Streaming or Compressed In Memory for long tracks instead.
5. Solution 4: Managed Code Stripping to Reduce Binary Size
Engine code and scripts themselves contribute to the initial WebAssembly binary footprint. Reduce this by stripping unused engine code.
- Go to Edit > Project Settings > Player.
- Expand Other Settings.
- Locate the Optimization section and set Managed Stripping Level to Medium or High.
This automatically strips unused assemblies, classes, and physics/audio/rendering subsystems from the final build, reducing the startup memory required to initialize the Wasm module.
Warning: Dealing with Stripping Errors
Aggressive stripping might remove code that is invoked dynamically via reflection or only referenced in Prefabs. To protect specific classes, create a
link.xml file in your Assets/ folder to specify assemblies and types that must be preserved.