Quick answer: Unity freezes during scene loading because SceneManager.LoadScene is synchronous by default - it blocks the main thread while loading all assets. Use SceneManager.

Here is how to fix Unity scene loading freezes game. You call SceneManager.LoadScene() and your game freezes for two, five, sometimes ten seconds. The frame rate drops to zero, audio stutters or stops, and the player stares at a frozen screen until the new scene eventually appears. This is one of the most common and most jarring issues in Unity games, and it happens because synchronous scene loading blocks the main thread entirely. Here is how to fix it properly with async loading, loading screens, and memory management.

Why Synchronous Scene Loading Freezes Your Game

When you call SceneManager.LoadScene("LevelTwo"), Unity performs every step of the scene load on the main thread, in a single frame. It reads the scene file from disk, deserializes all GameObjects and Components, loads all referenced textures, meshes, audio clips, and materials into memory, calls Awake() on every script, and finally activates the scene. Nothing else can happen during this process — no rendering, no input processing, no audio updates. The game is completely frozen.

For tiny scenes with a handful of objects, this takes a fraction of a second and you might not notice. But for real game levels with hundreds of objects, large textures, complex meshes, and multiple audio clips, the load can take several seconds. On mobile devices with slower storage and less RAM, it can take even longer.

The solution is SceneManager.LoadSceneAsync(), which spreads the loading work across multiple frames. The game keeps running — you can display a loading screen, animate a progress bar, and keep audio playing — while the scene loads in the background.

Basic Async Scene Loading

The simplest fix is to replace your synchronous load call with an async one inside a coroutine:

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class SceneLoader : MonoBehaviour
{
    public void LoadLevel(string sceneName)
    {
        StartCoroutine(LoadSceneCoroutine(sceneName));
    }

    private IEnumerator LoadSceneCoroutine(string sceneName)
    {
        // Start loading the scene in the background
        AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);

        // Wait until the scene is fully loaded
        while (!asyncLoad.isDone)
        {
            Debug.Log("Loading progress: " + asyncLoad.progress);
            yield return null;
        }
    }
}

This alone eliminates the freeze. The scene loads over multiple frames, and your game continues rendering and processing input during the load. However, there is still a brief hitch when the scene activates — the final 10% of loading involves activating all GameObjects and calling Awake(), which happens in a single frame. For large scenes, this activation hitch can still be noticeable.

Controlling Scene Activation with allowSceneActivation

The allowSceneActivation property gives you precise control over when the loaded scene actually becomes active. When set to false, Unity loads the scene up to 90% complete (the progress value reaches 0.9) and then pauses. The scene sits in memory, fully loaded but not yet activated. This is where you finish your loading screen animation, fade to black, or do any other transition work. When you are ready, set allowSceneActivation = true and the scene activates.

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;

public class LoadingScreenManager : MonoBehaviour
{
    [SerializeField] private Slider progressBar;
    [SerializeField] private CanvasGroup loadingScreenCanvas;
    [SerializeField] private float minimumLoadTime = 1.5f;

    private float loadStartTime;

    public void LoadScene(string sceneName)
    {
        StartCoroutine(LoadWithProgress(sceneName));
    }

    private IEnumerator LoadWithProgress(string sceneName)
    {
        loadStartTime = Time.time;

        // Show loading screen
        loadingScreenCanvas.alpha = 1f;
        loadingScreenCanvas.blocksRaycasts = true;

        // Start async load but prevent auto-activation
        AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
        asyncLoad.allowSceneActivation = false;

        // Update progress bar while loading
        while (asyncLoad.progress < 0.9f)
        {
            // Map 0-0.9 to 0-1 for the progress bar
            float displayProgress = Mathf.Clamp01(asyncLoad.progress / 0.9f);
            progressBar.value = displayProgress;
            yield return null;
        }

        // Loading is complete (at 0.9), set bar to full
        progressBar.value = 1f;

        // Ensure minimum display time so the screen doesn't flash
        float elapsed = Time.time - loadStartTime;
        if (elapsed < minimumLoadTime)
        {
            yield return new WaitForSeconds(minimumLoadTime - elapsed);
        }

        // Now activate the scene
        asyncLoad.allowSceneActivation = true;
    }
}

The minimum load time is important for user experience. If a scene loads in 0.1 seconds, the loading screen appears and vanishes instantly, which looks like a glitch. A minimum of 1-2 seconds gives the player time to register the transition and read any loading tips you display.

Note the progress mapping: Unity's AsyncOperation.progress goes from 0 to 0.9 during the loading phase. The final jump from 0.9 to 1.0 happens when allowSceneActivation is set to true and the scene activates. If you display the raw progress value on a slider, it will appear to get stuck at 90%. Dividing by 0.9 maps the range to 0-100%, which looks correct to the player.

Loading Screen Architecture

A robust loading screen pattern uses a persistent scene that never gets unloaded. This loading scene contains the UI, the camera, and the SceneLoader script. It loads and unloads game scenes additively, ensuring there is always something visible on screen.

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;

public class PersistentSceneLoader : MonoBehaviour
{
    [SerializeField] private Slider progressBar;
    [SerializeField] private GameObject loadingScreen;
    [SerializeField] private Camera loadingCamera;

    private string currentScene;

    void Awake()
    {
        DontDestroyOnLoad(this.gameObject);
    }

    public void TransitionToScene(string newScene)
    {
        StartCoroutine(PerformTransition(newScene));
    }

    private IEnumerator PerformTransition(string newScene)
    {
        // Show loading UI and camera
        loadingScreen.SetActive(true);
        loadingCamera.enabled = true;
        progressBar.value = 0f;

        // Unload current scene if one exists
        if (!string.IsNullOrEmpty(currentScene))
        {
            AsyncOperation unload = SceneManager.UnloadSceneAsync(currentScene);
            while (!unload.isDone)
            {
                yield return null;
            }

            // Free memory from the unloaded scene
            yield return Resources.UnloadUnusedAssets();
        }

        // Load new scene additively
        AsyncOperation load = SceneManager.LoadSceneAsync(
            newScene, LoadSceneMode.Additive);
        load.allowSceneActivation = false;

        while (load.progress < 0.9f)
        {
            progressBar.value = load.progress / 0.9f;
            yield return null;
        }

        progressBar.value = 1f;
        yield return new WaitForSeconds(0.5f);

        // Activate the new scene
        load.allowSceneActivation = true;
        yield return load;

        // Set the new scene as active (for lighting, etc.)
        Scene loadedScene = SceneManager.GetSceneByName(newScene);
        SceneManager.SetActiveScene(loadedScene);

        currentScene = newScene;

        // Hide loading UI
        loadingCamera.enabled = false;
        loadingScreen.SetActive(false);
    }
}

This pattern avoids the common problem where the loading screen itself gets destroyed during the scene transition. Because the loader lives in a persistent scene (protected by DontDestroyOnLoad), it survives scene changes and can always show feedback to the player.

The additive loading approach also lets you preload scenes before the player needs them. For example, you can start loading the next level in the background while the player is still playing the current one, making the transition near-instantaneous when they reach the end.

Using Addressables for Large Scenes

For games with large levels or many scenes, Unity's Addressable Asset System provides more granular control over what gets loaded and when. Instead of loading an entire scene at once, Addressables let you load individual assets, prefabs, or sub-scenes on demand.

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using System.Collections;

public class AddressableSceneLoader : MonoBehaviour
{
    private AsyncOperationHandle<SceneInstance> currentSceneHandle;

    public IEnumerator LoadAddressableScene(string sceneAddress)
    {
        // Unload previous scene if loaded via Addressables
        if (currentSceneHandle.IsValid())
        {
            var unloadOp = Addressables.UnloadSceneAsync(currentSceneHandle);
            yield return unloadOp;
        }

        // Load new scene via Addressables
        currentSceneHandle = Addressables.LoadSceneAsync(
            sceneAddress,
            UnityEngine.SceneManagement.LoadSceneMode.Additive);

        while (!currentSceneHandle.IsDone)
        {
            float progress = currentSceneHandle.PercentComplete;
            Debug.Log("Addressable load progress: " + progress);
            yield return null;
        }

        if (currentSceneHandle.Status == AsyncOperationStatus.Failed)
        {
            Debug.LogError("Failed to load scene: " +
                currentSceneHandle.OperationException);
        }
    }
}

Addressables also handle memory management automatically through reference counting. When you unload an Addressable scene, all assets that were loaded specifically for that scene get released. This prevents the memory leaks that commonly occur when using Resources.Load() or direct asset references without explicit cleanup.

For very large open-world games, you can split your world into chunks and load them as Addressable assets based on the player's position. This streaming approach keeps memory usage constant regardless of world size, because you only have the nearby chunks loaded at any time.

Memory Spikes During Scene Transitions

Even with async loading, scene transitions can cause memory spikes that crash mobile games. The spike happens because, for a brief period, both the old scene and the new scene exist in memory simultaneously. If each scene uses 500MB of memory and the device only has 1.2GB available to your app, the overlap pushes you over the limit.

The solution is to unload the old scene and release its assets before loading the new one. Use an intermediate empty scene or loading scene as a buffer:

using UnityEngine;
using UnityEngine.SceneManagement;
using System;
using System.Collections;

public class MemorySafeLoader : MonoBehaviour
{
    public IEnumerator LoadWithMemoryCleanup(string targetScene)
    {
        // Step 1: Load a minimal transition scene to replace the current one
        yield return SceneManager.LoadSceneAsync("LoadingScene");

        // Step 2: Force garbage collection to free old scene's memory
        yield return Resources.UnloadUnusedAssets();
        GC.Collect();
        GC.WaitForPendingFinalizers();

        // Step 3: Wait one frame to let memory settle
        yield return null;

        // Step 4: Now load the target scene with old memory freed
        AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(targetScene);
        asyncLoad.allowSceneActivation = false;

        while (asyncLoad.progress < 0.9f)
        {
            yield return null;
        }

        // Step 5: Activate when ready
        asyncLoad.allowSceneActivation = true;
    }
}

The key is the Resources.UnloadUnusedAssets() call between unloading the old scene and loading the new one. This is itself an async operation that can take several frames to complete, which is why we yield on it. It scans all loaded assets and releases any that are no longer referenced by any active object.

The GC.Collect() call forces the garbage collector to run immediately rather than waiting for its next scheduled pass. This is normally discouraged during gameplay because it causes a frame hitch, but during a loading screen, the hitch is invisible to the player and the freed memory is valuable.

Reducing Load Times with Preloading

The best scene transition is one the player never notices. If you know which scene the player will visit next, you can preload it in the background while they are still playing the current scene:

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class ScenePreloader : MonoBehaviour
{
    private AsyncOperation preloadedScene;
    private string preloadedSceneName;

    /// Call this early, e.g. when the player is halfway through the level
    public void PreloadScene(string sceneName)
    {
        if (preloadedScene != null) return; // Already preloading

        preloadedSceneName = sceneName;
        StartCoroutine(DoPreload(sceneName));
    }

    private IEnumerator DoPreload(string sceneName)
    {
        preloadedScene = SceneManager.LoadSceneAsync(sceneName);
        preloadedScene.allowSceneActivation = false;

        // Let it load in the background
        while (preloadedScene.progress < 0.9f)
        {
            yield return null;
        }

        Debug.Log("Scene preloaded and ready: " + sceneName);
    }

    /// Call this when the player actually triggers the transition
    public void ActivatePreloadedScene()
    {
        if (preloadedScene != null)
        {
            // Scene is already loaded, just activate it
            preloadedScene.allowSceneActivation = true;
            preloadedScene = null;
            preloadedSceneName = null;
        }
    }
}

With preloading, the scene transition appears nearly instantaneous to the player because all the heavy loading happened in the background over the previous 30 seconds of gameplay. The only delay is the final scene activation, which typically takes a single frame.

Be careful with preloading on memory-constrained devices. A preloaded scene sits in memory alongside the current scene, so you are effectively doubling your memory usage until the old scene is unloaded. Profile your memory usage to make sure both scenes fit simultaneously.

Related Issues

If your game runs fine but specific assets within a scene take too long to load, see our guide on fixing Unity texture memory leaks, which covers efficient texture loading and unloading patterns. For games that crash during scene loads on mobile devices, fixing Unity Android build crashes covers memory limits and out-of-memory errors specific to Android. And if your loading screen itself appears blank or broken, fixing invisible sprite renderers covers common UI rendering issues.

Always load scenes async. There is no excuse for freezing the player's screen.