← Back to Tutorials
Advanced~16 min readUE 5.5 / 5.6

Loading Time Optimization

UE 5.5 narrowed BlockTillLevelStreamingCompleted's scope from "flush every in-flight async load" to "flush only this streaming level's requests" — reducing PIE entry time on large projects significantly. UE 5.6 added unified streaming budgets. UE 5.7 adds StaticLoadAsset and LoadAssetAsync for ergonomic async paths. This tutorial walks the soft-vs-hard-reference decision, FStreamableManager lifecycle, level streaming budgets, blocking-load detection in Insights, and the patterns that make UE5 boot in 5 seconds.

1

The loading pipeline in 5.5/5.6

UE5 loading flows through:

  • Asset Registry — metadata lookup; warmed at boot.
  • Zen Loader (5.5+ default) — replaces deprecated EDL. Cuts runtime dependency-graph processing "from seconds to milliseconds" by moving prep offline.
  • FStreamableManager — the async load coordinator.
  • Level StreamingUWorld::AddToWorld pipeline; per-tick time budgets.

Critical 5.5 change: UWorld::BlockTillLevelStreamingCompleted now flushes only outstanding ULevelStreaming::AsyncRequestIDs rather than all global async loads. Set s.World.ForceFlushAllAsyncLoadsDuringLevelStreaming=1 to revert if your custom ULevelStreaming subclass relies on the old behavior.

2

Hard vs soft references

A hard reference (UPROPERTY() UStaticMesh* Mesh) means the asset loads when the owner does. A soft reference (UPROPERTY() TSoftObjectPtr<UStaticMesh> Mesh) means the asset loads only when explicitly requested via LoadSynchronous() or RequestAsyncLoad.

Hard references cascade. A Blueprint that hard-refs a 200MB hero mesh on its CDO pulls that 200MB into memory at level load — even if the actor never spawns. Soft refs let you load on demand.

The trap covered in Gotcha #3: Cast nodes create hard references too, so "avoiding casts" is sometimes correct (when the goal is reducing the hard-ref footprint), but the underlying issue is reference type, not Cast specifically.

The four soft pointer types and their methods:

  • TSoftObjectPtr<T> — specific asset reference (e.g., a particular UStaticMesh).
  • TSoftClassPtr<T> — specific class reference (e.g., a Blueprint class to spawn).
  • FSoftObjectPath — untyped path; serializable string form.
  • FSoftClassPath — untyped class path.

Methods on TSoftObjectPtr: Get() returns the raw T* if already loaded (nullptr otherwise; does not load); LoadSynchronous() blocks the game thread to load; IsPending() tests whether load is still queued; ToSoftObjectPath() converts to FSoftObjectPath.

3

FStreamableManager patterns

C++ idiomatic async load
FSoftObjectPath AssetPath = MySoftRef.ToSoftObjectPath();
TSharedPtr<FStreamableHandle> Handle = UAssetManager::GetStreamableManager().RequestAsyncLoad(
    AssetPath,
    FStreamableDelegate::CreateLambda([this, AssetPath]()
    {
        UStaticMesh* Loaded = Cast<UStaticMesh>(AssetPath.ResolveObject());
        // Use Loaded
    }),
    FStreamableManager::AsyncLoadHighPriority,
    false,                           // bManageActiveHandle: false = caller owns handle
    false,                           // bStartStalled
    TEXT("MyDebugName"));               // Helps in Insights

// Store Handle as a member to keep the asset alive!
ActiveLoadHandle = Handle;
The handle keeps the load alive Store FStreamableHandle as a member, not in a local that goes out of scope. Per Tom Looman: storing the handle in a local variable that ends at end of BeginPlay is the #1 "why did my texture pop in" bug. Preload auto-releases when handle drops; Load keeps it pinned.
4

Asset Manager & Primary Asset Types

Asset Manager is the senior pattern for organizing async loads. Define Primary Asset Types in DefaultGame.ini:

DefaultGame.ini
[/Script/Engine.AssetManagerSettings]
+PrimaryAssetTypesToScan=(PrimaryAssetType="Monster",AssetBaseClass="/Script/MyGame.MonsterData",
                          bHasBlueprintClasses=False,bIsEditorOnly=False,
                          Directories=((Path="/Game/Monsters")),
                          SpecificAssets=,Rules=(CookRule=AlwaysCook))

Then load with:

C++
UAssetManager& Manager = UAssetManager::Get();
FPrimaryAssetId MonsterId(TEXT("Monster"), FName(TEXT("Goblin_Boss")));
TSharedPtr<FStreamableHandle> Handle = Manager.LoadPrimaryAsset(
    MonsterId,
    {TEXT("Combat"), TEXT("UI")}, // Asset Bundles to load
    FStreamableDelegate::CreateUObject(this, &UMyClass::OnMonsterLoaded));

Asset Bundles (declared via meta=(AssetBundles="UI,Combat") on UPROPERTYs) let you partial-load a primary asset — saving 30–70% RAM on heavy data assets without changing the type.

5

Level streaming budgets

The per-tick cost knobs:

  • s.LevelStreamingActorsUpdateTimeLimit — per-frame seconds budget for AddToWorld().
  • s.PriorityLevelStreamingActorsUpdateExtraTime — extra time for high-priority levels.
  • s.LevelStreamingComponentsRegistrationGranularity — components registered per AddToWorld() slice.
  • s.GLevelStreamingForceGCAfterLevelStreamedOut=0 to suppress forced GC after unload (often the visible hitch right after a sublevel goes "Unloaded").

UE 5.6 adds s.UseUnifiedTimeBudgetForStreaming — unified budget across ProcessAsyncLoading and UpdateLevelStreaming; donates unused streaming time to asset loading.

6

Detecting blocking loads in Insights

Capture flags
YourGame.exe -trace=loadtime,loadtimechannel,asset,assetmetadata,frame,bookmark

Open the trace in Insights → Asset Loading panel (UE 5.5+: enable via the "Other" dropdown if not visible). Five tabs: Event Aggregation, Object Type Aggregation, Package Details, Export Details, Requests.

Look for the "Flush All Async Loads GT" marker (5.5+) — any occurrence after the loading screen closes is a blocking load you can fix. Also use LoadTimes.DumpReport [FILE] [LOWTIME=0.05] for per-package load-time tables filtered above 50 ms.

The Asset Loading Insights panel doesn't appear without -trace=loadtime Teams routinely capture sessions and miss the entire bottleneck because they didn't include the loadtime channel.
7

IoStore + Zen Loader

IoStore (.utoc/.ucas containers) replaces .pak as the cooked output format for shipping UE5. Zen Loader runs on top of IoStore by default in 5.5+:

  • Zen Loader chunk graph — loaded chunks are dependency-resolved at cook time, not runtime.
  • FileOpenOrder — cook-time on-disk ordering. Even on NVMe, sequential layout helps.
  • IoStoreOnDemand — install-on-demand patterns; only download chunks the player needs.

EDL (Event-Driven Loader) is deprecated. Turning off IoStore (bUseIoStore=False) to "fix" a packaging issue puts you on a removal path.

8

Common anti-patterns

  • StaticLoadObject in gameplay code. Triggers FlushAsyncLoading, draining the entire async queue on the game thread. Mixed into otherwise-async code, creates correlated hitches that look like GC spikes.
  • LoadSynchronous on a soft ref from BeginPlay of a streamed actor. Stalls the game thread for the entire async batch.
  • Cyclic soft refs — two data assets soft-referencing each other will load each other on first dereference. Soft refs don't appear in Reference Viewer's hard-graph; use Size Map and AssetRegistry filtering.
  • Asset bundle name typos — bundle names are case-sensitive FNames; a typo silently loads zero refs.
  • Forgetting bManageActiveHandle=true from a transient context — without it, the manager's active list won't pin the load and you race against GC.
  • Storing FStreamableHandle only in a local stack variable — handle goes out of scope at end of method; load gets cancelled. Always store as a class member.
UE-217968: PIE soft-path CDO corruption A soft-pointer load during PIE can rewrite the Blueprint CDO to a PIE-instance path (something like /Game/UEDPIE_0_MyAsset). If that CDO survives a save, it breaks packaged builds. Mitigation: FSoftObjectPath::FixupForPIE at the right point, and audit any tooling that saves assets after PIE has run. Tracked at issues.unrealengine.com/issue/UE-217968.

Packaged-build ResolveObject() vs TryLoad() behave differently — ResolveObject only succeeds if the asset is already loaded; TryLoad attempts a synchronous load. Don't use TryLoad as a fast path.

What to ban from BeginPlay

The pre-submit checklist:

  1. No StaticLoadObject — ever, in any code path.
  2. No LoadSynchronous on cold soft refs — use RequestAsyncLoad with a callback.
  3. No hard UPROPERTY(UClass*) on data assets — soft-class refs only.
  4. No construction-script-heavy actors with hard asset references. Construction scripts re-run on AddToWorld and bypass async loading.
  5. No BlockTillLevelStreamingCompleted outside fast-travel hard boundaries.
  6. Always store FStreamableHandle as a member, not a local.

PerfGuard can boot a cooked build with -trace=loadtime,asset,assetmetadata,frame + -LoadTimeFile, open a target map, and stop on a "ready" bookmark. Diff four numbers vs baseline: total package load time, count of packages over 100ms, count of "Flush All Async Loads GT" markers (5.5+), per-level AddToWorld time. CI gate: any new package over 100ms or any new GT-flush marker fails the PR.