Unreal 5.6 C++ – NavMesh Performance: Synchronous vs Timer+Cache vs Async

This prototype focuses on a common but costly performance mistake in Unreal Engine: calling FindPathToActorSynchronously inside TickComponent. The scenario is intentionally minimal — hundreds of moving actors continuously pathfinding toward a single target — so the performance impact is the subject under study, not the game design.

The core of the demo is UNavTargetTrackerComponent, an ActorComponent that pathfinds toward any AActor tagged NavTarget in the level. It exposes an EPathfindingMode enum that can be switched at runtime in the Editor Details panel — no recompile needed — making it easy to compare the three strategies live while measuring with Unreal Insights.

Synchronous (Buggy Baseline). FindPathToActorSynchronously is called every TickComponent. This blocks the Game Thread for the full duration of the NavMesh query on every frame. With 289 actor instances running simultaneously, Unreal Insights showed a Game Thread stall of ~38ms per frame — roughly 2.5x the 16ms budget for 60 FPS — dropping the framerate from 120 FPS to 12 FPS.

Timer + Cache. An FTimerHandle triggers a synchronous path recalculation at 5Hz (every 0.2s) instead of every frame. The result is stored in a TArray<FVector> CachedPathPoints. TickComponent reads from this cache without touching the NavMesh at all, reducing Game Thread stalls from 120x/sec to 5x/sec. Framerate recovered to 120 FPS with the same 289 instances.

Async. The most complete solution. FindPathAsync launches the NavMesh query on a background navigation thread via an FNavPathQueryDelegate callback (OnPathFound). The Game Thread is never blocked — it simply reads CachedPathPoints on every Tick, exactly as in Timer+Cache mode, but the cache is now written asynchronously when the background thread completes the query. Any in-flight request is cancelled via AbortAsyncFindPathRequest before a new one is launched, preventing stale results from overwriting a more recent cache. EndPlay cancels any pending request to avoid dangling callbacks after the component is destroyed.

The key insight across all three modes is that TickComponent should never own expensive work. Its job is to read state that was computed elsewhere — by a timer, by a background thread, or by any other mechanism that doesn’t hold the Game Thread hostage.

GitHub Repository

Leave a comment

Create a website or blog at WordPress.com

Up ↑