
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.
Methodology & Test Setup
This experiment was designed as a deliberately narrow stress test for pathfinding behavior rather than as a representative or hardware-normalized benchmark.
- Unreal Engine: 5.6
- Actors running
UNavTargetTrackerComponent: 289 - Navigation target: 1 actor tagged
NavTarget - Profiler: Unreal Insights
Detailed hardware, build configuration, capture duration, warm-up, and repeated-run metadata were not recorded with the original experiment. For that reason, the results should be interpreted as evidence about the architectural behavior of these pathfinding approaches in this specific stress scenario, not as portable performance numbers for other hardware or projects.
The three implementations also do not execute an identical amount of navigation work. The experiment compares architectural strategies and their practical trade-offs rather than equal-throughput workloads.
Synchronous baseline
FindPathToActorSynchronously is called from TickComponent, so each component can issue one synchronous NavMesh query per frame.
With 289 active components, Unreal Insights showed approximately 38 ms of observed Game Thread work associated with the synchronous pathfinding scenario. The test also showed the scenario dropping from approximately 120 FPS to 12 FPS.
Those two numbers should not be interpreted as direct equivalents. A 38 ms frame would correspond to roughly 26 FPS if it represented the complete frame time. A sustained 12 FPS corresponds to approximately 83 ms per frame.
Therefore, the 12 FPS result cannot be explained by the 38 ms measurement alone. The ~38 ms value represents an observed Game Thread cost in the captured profiling data, while the FPS number represents the total behavior of the scenario, which includes additional engine, navigation, rendering, scheduling, and frame costs that were not isolated by this experiment.
The useful conclusion is not that “38 ms causes 12 FPS,” but that synchronous pathfinding was a major Game Thread bottleneck in this stress scenario.
Timer + Cache
The timer-based implementation recalculates the path at 5 Hz (every 0.2 seconds) and stores the result in CachedPathPoints. TickComponent only consumes the cached result.
This dramatically reduces how often synchronous navigation work is requested. However, this comparison is intentionally not an equal-workload benchmark.
At 120 FPS, the synchronous implementation can request roughly 120 path calculations per second per component, while the timer version requests only 5. For 289 components, that corresponds theoretically to as many as 34,680 synchronous requests/second at 120 updates per second versus 1,445 requests/second at 5 updates per second.
The timer version is faster partly because it performs far less pathfinding work. Its trade-off is lower path freshness: actors may follow a cached route for up to roughly 0.2 seconds before receiving an updated path. That can be an excellent engineering trade-off when targets and navigation conditions do not require frame-perfect updates.
Async
FindPathAsync changes a different part of the problem: instead of blocking the calling Game Thread while the query completes, the navigation request finishes asynchronously and updates the cached path through a callback.
This improves Game Thread responsiveness, but asynchronous pathfinding is not free.
- Results arrive later rather than immediately.
- The cached route can temporarily represent older world state.
- Request frequency still needs to be controlled.
- Repeated cancellation and resubmission can create unnecessary work.
- Actor/component lifetime must be handled safely while callbacks are pending.
- Stale or superseded results must not overwrite newer path data.
Async is therefore primarily a tool for decoupling expensive work from the caller, not a guarantee that navigation itself becomes cheaper.
What the experiment actually demonstrates
The three approaches illustrate different architectural strategies rather than a perfectly controlled throughput benchmark:
- Synchronous Tick prioritizes maximum path freshness but can severely block the Game Thread.
- Timer + Cache reduces computation by accepting less frequent path updates.
- Async + Cache reduces caller blocking and allows path computation to complete independently, at the cost of latency and greater lifecycle complexity.
The broader engineering lesson is not “never perform work in Tick.” Tick is often the correct place to consume continuously changing state.
The problem is performing expensive synchronous work every frame when the gameplay requirement does not actually demand frame-level recomputation.
