DEV Community

GameDevToolLab
GameDevToolLab

Posted on

Practical UE5 File Loading Optimization: Beyond Parallel I/O and Raising the FPS Cap

UE5 loading optimization is often reduced to two ideas: read more files in parallel, and raise the FPS cap during a loading screen. Both can help, but only when they target the real bottleneck.

A transition also includes dependency discovery, decompression, UObject creation, Serialize and PostLoad, activation, collision, texture/audio streaming, PSOs, and GC. If activation dominates the Game Thread, more parallel reads may only create a larger completion spike.

This article targets UE 5.8 as of August 2026. Use this order:

  1. Measure I/O and post-load work separately.
  2. Reduce hard-reference graphs and requested data.
  3. Preload by player-experience phase.
  4. Change loading budgets during controlled transitions instead of blindly unlocking FPS.
  5. Tune containers, compression, streaming, and activation on target hardware.

Read by symptom

  • Slow startup: reference graphs, Asset Manager, IoStore.
  • Teleport hitch: World Partition sources, texture mips, PSOs.
  • Freeze after the loading screen: activation, overlap, PSOs.
  • Slow custom JSON or binary data: split Read, Decode, and Apply.

Decompose load time first

Treat loading as a pipeline:

Stage Typical work Common bottleneck
Discover Asset Registry, Primary Assets, dependencies Large search scope or graph
Read Container or file reads Random I/O, many small requests
Decode Decryption, Oodle decompression CPU saturation
Deserialize UObject creation, Serialize, PostLoad Custom work, nested sync loads
Activate Actor spawn, Component registration, render setup Game Thread, collision, PSOs

If Read takes 0.4 seconds and Activate takes 2.0 seconds, more I/O concurrency cannot remove most of the wait. It may make more packages finish together and worsen the activation spike.

Record median and p95 transition time, worst Game Thread frame, bytes and request count, decode CPU, activation time, and peak memory. Separate cold and warm-cache runs. Use cooked packaged builds on target devices for final decisions.

Separate loading from waiting with Unreal Insights

In UE 5.8, AssetLoadTime exposes timing around asset serialization, while LoadTime covers runtime loading from pak or IoStore paths:

-trace=default,AssetLoadTime,LoadTime
Enter fullscreen mode Exit fullscreen mode

Use Editor or PIE for reference investigation, then compare a Shipping-equivalent packaged build with the same channels, device, and cache conditions. Add -statnamedevents only when Blueprint names are necessary, and place bookmarks around the transition.

Check for:

  • I/O or Async Loading Thread starvation.
  • PostLoad, Actor creation, or registration spikes.
  • Duplicate requests or urgent work buried under broad preloads.
  • Texture or PSO hitches after package loading completes.

Focused Zen Loader logging can help:

-LogCmds="LogStreaming veryverbose"
Enter fullscreen mode Exit fullscreen mode

Do not keep VeryVerbose logging in normal profiling builds.

Avoid polling GetAsyncLoadPercentage every frame. Epic warns that it is slow and may block async loading. Prefer load groups, handle callbacks, and monotonic phase-based progress.

The largest optimization is not loading the asset

Hard references such as UTexture2D*, USkeletalMesh*, and Blueprint classes pull dependency graphs into memory. A persistent “all characters” registry can make the title screen load meshes, materials, animation, audio, and VFX. Inspect inclusive size, dependency direction, and cooked chunks with Reference Viewer, Size Map, and Asset Audit.

When owner and target lifetimes differ, use soft references:

UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<USkeletalMesh> CharacterMesh;

UPROPERTY(EditDefaultsOnly)
TSoftClassPtr<AActor> CharacterClass;
Enter fullscreen mode Exit fullscreen mode

A soft reference does not load its target automatically. Request it through Streamable Manager or Asset Manager when its phase begins.

Changing the type does not guarantee cooking. The Cooker still needs a discoverable route through a serialized UPROPERTY, Asset Bundle, Primary Asset rule, PrimaryAssetLabel, explicit Cook Rule, or similar mechanism. Runtime-generated soft paths need separate cook registration. Verify final chunk membership in a packaged build.

Keep hard references when assets are always required together. Otherwise inspect ConstructorHelpers, Blueprint types and casts, class defaults, and distant World Partition Actor links. Interfaces, IDs, Gameplay Tags, events, or subsystem queries can avoid loading a concrete class.

Load by experience phase with Asset Manager

Individual soft references become difficult to own and release consistently. Centralize policy around Asset Manager and divide Secondary Assets into named Asset Bundles:

UCLASS(BlueprintType)
class UCharacterDefinition : public UPrimaryDataAsset
{
    GENERATED_BODY()
public:
    UPROPERTY(EditDefaultsOnly, meta=(AssetBundles="Menu"))
    TSoftObjectPtr<UTexture2D> Portrait;

    UPROPERTY(EditDefaultsOnly, meta=(AssetBundles="Gameplay"))
    TSoftObjectPtr<USkeletalMesh> Mesh;

    UPROPERTY(EditDefaultsOnly, meta=(AssetBundles="Gameplay"))
    TSoftClassPtr<AActor> CharacterClass;

    UPROPERTY(EditDefaultsOnly, meta=(AssetBundles="Voice"))
    TArray<TSoftObjectPtr<USoundBase>> VoiceAssets;
};
Enter fullscreen mode Exit fullscreen mode

Load Menu during selection, Gameplay after confirmation, and Voice before dialogue. Bundle by player-experience phase, not only file type.

This UE 5.8 excerpt shows ownership and request replacement for one character. It omits declarations, logging, retries, and multi-consumer counting; older versions may use earlier overloads.

void UStageLoadSubsystem::LoadCharacter(const FPrimaryAssetId& Id)
{
    if (LoadedCharacterId == Id) return;

    ReleaseCharacter();
    LoadedCharacterId = Id;

    FAssetManagerLoadParams Params;
    Params.Priority = FStreamableManager::DefaultAsyncLoadPriority;
    Params.OnComplete = FStreamableDelegateWithHandle::CreateWeakLambda(
        this, [this, Id](TSharedPtr<FStreamableHandle>)
        {
            if (LoadedCharacterId != Id) return;
            if (UObject* Asset = UAssetManager::Get()
                    .GetPrimaryAssetObject(Id))
            {
                OnCharacterReady(Asset);
            }
        });

    const TArray<FName> Bundles{ FName(TEXT("Gameplay")) };
    CharacterHandle = UAssetManager::Get().LoadPrimaryAsset(
        Id, Bundles, MoveTemp(Params));
}

void UStageLoadSubsystem::ReleaseCharacter()
{
    if (CharacterHandle.IsValid() &&
        !CharacterHandle->HasLoadCompleted())
    {
        CharacterHandle->CancelHandle();
    }
    CharacterHandle.Reset();

    if (!LoadedCharacterId.IsValid()) return;
    UAssetManager::Get().UnloadPrimaryAsset(LoadedCharacterId);
    LoadedCharacterId = FPrimaryAssetId();
}
Enter fullscreen mode Exit fullscreen mode

Production code still needs request generations, consumer counts, and separate failure and cancellation signals.

API Lifetime model
LoadPrimaryAsset(s) Retained until UnloadPrimaryAsset(s). The handle controls progress, waiting, cancellation, and callbacks, not asset lifetime.
PreloadPrimaryAssets Valid while its handle is retained; releasable afterward if unreferenced.
Direct Streamable Manager request A retained handle or hard reference owns the useful lifetime.

LoadPrimaryAsset(s) may return a null handle when no new work is required while still invoking completion. Validate the object in OnComplete, keep failure/cancellation/replacement distinct, and put release under one owner.

Parallel loading is not “the more, the faster”

Unlimited concurrency can create small random reads, decode contention, registration bursts, high peak memory, and urgent-work starvation. Group requests by player need:

  1. Minimum: map, player, required UI.
  2. Immediate: nearby assets, first enemies, essential SFX.
  3. Later: next area, additional enemies, dialogue voice.
  4. Optional: distant high mips and cosmetics.

Only the minimum set should receive high priority. Share in-flight handles and track consumers to prevent duplicate requests and premature unloads.

Cancellation does not rewind completed work. Treat request cancellation, handle release, and UObject-reference removal as separate operations.

Do not represent readiness with one boolean

An async callback rarely means gameplay is safe to start. Track at least:

  1. Requested
  2. AssetsReady
  3. WorldReady
  4. RuntimeReady
  5. PresentationReady
  6. Interactive

After a handle completes, spawning, collision, UI, network synchronization, texture/audio readiness, and PSOs may remain. Aggregate completion from the owning subsystems.

Use measured phase weights for monotonic display progress:

AssetsReady       35%
WorldReady        25%
RuntimeReady      20%
PresentationReady 15%
Interactive        5%
Enter fullscreen mode Exit fullscreen mode

Keep display progress separate from the internal state machine.

Can a higher FPS cap make loading faster?

Sometimes. Streaming and Actor/Component registration include per-frame budgeted work. Raising the FPS cap may apply those budgets more often per second.

It will not help when storage, decompression CPU, or one long Game Thread task is saturated. It may hurt when rendering, animation, UI, or unrelated Tick consumes the extra frames. Treat t.MaxFPS 0 as a diagnostic experiment, not a permanent strategy.

During gameplay, stable frame time matters. During a controlled transition, total completion time may matter more:

Example setting Purpose
s.AsyncLoadingTimeLimit Per-frame time for async loading
s.PriorityAsyncLoadingExtraTime Extra time for priority loading
s.LevelStreamingActorsUpdateTimeLimit Time for streamed Actor updates
s.PriorityLevelStreamingActorsUpdateExtraTime Extra priority streaming time
s.LevelStreamingComponentsRegistrationGranularity Registration batch size
s.UnregisterComponentsTimeLimit Time for unregister work

Names and behavior vary by version and platform. Check VariableName ?, Engine Source, Device Profiles, and current values. Centralize changes and restore prior values on success, cancellation, error, or map transition. Use Gameplay, Transition, and Tail profiles; wait only for the mandatory remainder in Tail.

Keep the loading screen cheap: heavy 3D scenes, Niagara, blur, Scene Capture, animated UI, and unnecessary Tick compete with loading. Preserve required render preparation or the hitch moves to the first interactive frame.

Avoid routine synchronous flushes

Calling FlushAsyncLoading immediately after an async request recreates the Game Thread stall it was meant to avoid. It is easier to justify at startup, in a fully non-interactive transition, or in deterministic editor and automation workflows.

If a synchronous boundary is unavoidable, start early, keep the mandatory set small, and wait only for the remaining tail. LoadSynchronous is not safe merely because the directly referenced asset looks small; its hard-reference graph may be large.

Tune IoStore, Zen Loader, and Oodle together

Zen Loader uses cooked dependency data and retrieves chunks from .utoc and .ucas containers. IoStore is the normal UE 5.8 packaged path.

Use Asset Registry for packaged discovery. Place custom data deliberately as a UE asset, staged UFS file, Non-UFS file, or Save/Download data; packaged Content is not a loose-file directory.

Inspect final layout when chunk placement looks wrong:

UnrealPak.exe IoStore -Describe=<Global.ucas> -DumpToFile=Output.txt
Enter fullscreen mode Exit fullscreen mode

Encrypted containers also need -CryptoKeys=<Crypto.json>.

Compression trades storage reads for decode CPU. Oodle Method changes the main size/decode-speed tradeoff; Level changes encoder effort and output size without selecting another runtime decoder. Smaller output can still reduce reads and patch size.

Method General tendency
Kraken Strong compression and good decode speed; useful baseline
Mermaid Less compression, faster decode
Selkie Favors decode speed further
Leviathan Smaller data is possible, with slower decode

Compare total time, decode CPU, container size, bytes read, and peak memory on weak-CPU and weak-storage targets. No compression may remove decode work while increasing I/O, download, and patch size.

World Partition is not only Cell Size

Small cells increase request count; large cells include unnecessary Actors. Tune for movement speed, visibility, Actor density, and storage. Before adding Runtime Grids, use HLOD Layers, Data Layers, and spatial loading.

For teleports, create a World Partition Streaming Source at the destination before moving the player:

  1. Configure grid, priority, and target state.
  2. Load during a fade, door animation, or elevator ride.
  3. Move after streaming completion.
  4. Disable the temporary source and release the old area.

Direct references between distant Actors can couple their cells. Replace them with IDs, events, or subsystem queries where appropriate.

Increasing wp.Runtime.MaxLoadingLevelStreamingCells may shorten elapsed time while raising CPU, memory, and registration peaks. Measure gameplay and transitions separately. UE 5.8 includes World Partition Insights; use branch-appropriate tools on older versions.

Optimize activation and second-stage resources

A completed read can still be followed by expensive spawning, registration, collision, and overlap work.

  • Consider ISM/HISM for repeated static geometry.
  • Avoid excessive Actor counts for non-gameplay decoration.
  • Remove always-present Components that are rarely needed.
  • Avoid global Actor searches and synchronous loads in BeginPlay.
  • Delay nonessential initialization until later frames or proximity.

Initial overlap calculation can be expensive. Evaluate UpdateOverlapsMethodDuringLevelStreaming and “Generate Overlap Events During Level Streaming” per class: decoration may not need it, while triggers may depend on it.

A package can be loaded while presentation data is still missing:

  • Textures: prioritize critical UI and near-field mips; do not mark everything Never Stream.
  • Audio: stream long assets, but prepare the first chunk for sounds that must start immediately.
  • Shaders/PSOs: use PSO Precaching or Pipeline Cache for materials guaranteed to appear at startup.

Prepare mandatory presentation data and continue optional resources in the background.

Custom files need Read, Decode, and Apply stages

Many worker tasks using synchronous file APIs can still create competing small reads. Choose storage first, then the API:

  • Small text: FFileHelper::LoadFileToStringAsync.
  • Large sequential processing without one full allocation: FFileHelper::LoadFileInBlocks.
  • Async offset-and-size reads: IAsyncReadFileHandle::ReadRequest.
  • Custom cooked chunks: consider FIoDispatcher with Asset Registry and chunk rules.

LoadFileInBlocks calls a visitor for sequential blocks. It avoids retaining the whole file, but does not guarantee asynchronous I/O. Keep it off the Game Thread. Use IAsyncReadFileHandle::ReadRequest for true async range reads.

Validate size and read requests; a non-null OpenAsyncRead handle is not full success. Keep each IAsyncReadRequest alive until completion and define one owner for the handle, request, and buffer.

Use this pipeline:

  1. Read: obtain bytes with async I/O.
  2. Decode: decompress, validate, and parse on a worker.
  3. Apply: create or update necessary UObjects on the Game Thread.

Keep parsed data in plain structures until Apply. Avoid retaining compressed input, decompressed output, and final objects simultaneously. For random access, index the format and read only required ranges.

Prefetch from player intent and control memory overlap

Distance-only prefetch is unreliable with fast movement and branching paths. Strong signals include a confirmed stage, destination, character selection, door sequence, matchmaking result, quest update, or server instruction.

Scale requests to confidence: hover may load a portrait, confirmation loads Gameplay, and an irreversible transition loads voice. Track hit rate, time hidden, wasted bytes, and residency because wrong predictions still consume work.

Preloading the next stage while retaining the old one can overlap working sets and trigger GC or OS pressure:

  1. Prefetch the new minimum set while the old scene remains visible.
  2. Stop expensive old-scene systems after the fade begins.
  3. Release old handles and references.
  4. Place necessary GC in a known non-interactive interval.
  5. Activate the new scene and continue optional loading.

Use Memory Insights before adding more manual GC. Compare peak memory beside total time when tuning concurrency.

Fast Geometry Streaming is not the default answer

UE 5.8 improves the Experimental Fast Geometry Streaming plugin for static, non-gameplay assets. It may help very large static worlds, but does not replace reference cleanup, Asset Manager, World Partition, or gameplay Actor loading. Validate platform and workflow constraints before adoption.

A practical workflow

  1. Fix device, build, storage, and starting state; record median, p95, worst hitch, and peak memory.
  2. Classify Read, Decode, Deserialize, and Activate in Insights.
  3. Remove unnecessary hard references and define mandatory/immediate/later/optional bundles.
  4. Add intent-driven preloading and Gameplay, Transition, and Tail budgets.
  5. Compare IoStore, Oodle, mips, audio, and PSOs under identical conditions.
  6. Add regression limits for dependency size, transition time, hitch, and memory.

Conclusion

UE5 loading performance is not determined by one concurrency number.

  • The largest wins usually come from loading fewer dependencies and dividing assets by experience phase.
  • Parallel I/O and a higher FPS cap should follow measurement of I/O, CPU, Game Thread, and memory behavior.
  • IoStore, Oodle, World Partition, activation, texture residency, audio, PSOs, and GC all affect perceived loading.
  • Custom data needs explicit Read, Decode, Apply, cancellation, and ownership boundaries.

Find the slow stage in Unreal Insights and why each asset is reachable in Reference Viewer. Apply parallelism, prefetching, budgets, and compression only where measurements support them. The goal is to become interactive without a large hitch afterward.

References

Checked against UE 5.8 documentation on August 9, 2026. For another engine version, verify the matching documentation and Engine Source.


Editorial disclosure: this English edition was prepared with AI-assisted translation and editing from a technically reviewed Japanese draft. The technical claims and code excerpts were reviewed against the references above before publication.

Top comments (0)