Unreal · Performance
Your UE5 game runs at 24fps. What now?
Before the LOD pass, the animation rewrite or the inevitable argument about Lumen: take a trace and find out which bit is actually slow.
Someone has a controller in one hand and stat fps on screen. The number says 24. A few people gather round and, within a minute or two, somebody says, “It’ll be Lumen.”
It might be. It usually isn’t quite that simple.
The costly bit of performance work is not making an optimisation which only saves half a millisecond. It is spending several weeks fixing a system which was never limiting the frame in the first place. I have seen a large LOD pass aimed at a render-thread problem, and an animation rewrite launched largely because animation felt like a plausible suspect. Both were serious bits of work. Neither began with a convincing measurement.
So, before discussing the usual culprits, I would get three things from the actual target console:
stat unit, from a representative bad scene in a sensible test configuration;- an Unreal Insights trace covering the same run; and
- a GPU capture, plus
ProfileGPUif I want a quick first look.
stat unit
stat unitgraph
ProfileGPU
Not from the editor. Not from the programmer’s very expensive PC. A console has its own CPU layout, memory limits, bandwidth and storage behaviour. The editor build can be useful, of course, but it is not the thing we are trying to ship.
I write down Frame, Game, Draw, GPU and RHIT. Even that small table tends to improve the conversation. “The game is slow” has become “Draw is taking 38ms in this scene”, which is at least a problem one can investigate.
The expensive-looking scene which is actually CPU-bound
This is the one I meet most often. The scene looks complicated, therefore everyone assumes the GPU is struggling to shade it. In fact the GPU has room left and the CPU is taking too long to assemble and submit the work.
The Draw and RHIT numbers are the first clue. In Insights I then look at the render and RHI threads: lots of small submissions, state changes, visibility work and scene traversal can cost a surprising amount before the GPU gets to do anything useful.
A quick resolution-scale experiment helps too. If a large drop in resolution barely changes the frame time, I am unlikely to be limited by pixel cost. It is not absolute proof - some GPU work is insensitive to resolution - but it is a good way to stop the meeting drifting straight into texture sizes and shader complexity.
The fix may be fewer distinct things to submit, better instancing, less material or component variety, or a change to how the scene is put together. The capture should lead that decision. “Optimise the art” is not a diagnosis.
The hitch which gets blamed on rendering
Next I would separate a consistently slow frame from a hitch. They feel different in the hands and they have rather different causes, but a single average-FPS figure cheerfully hides the distinction.
Shader and pipeline-state compilation is an obvious suspect when the first visit to an area is bad and the second is mysteriously better. Start from a cold boot, enter content the machine has not seen in that run, and keep the trace. Compilation or PSO creation tends to leave fairly recognisable evidence.
If it is not shaders, the trace will often point somewhere less glamorous: garbage collection, streaming, blocking I/O, audio, physics, networking, analytics or an innocent-looking synchronous load. World Partition can be involved, although “World Partition” is still only the name of an area to inspect. I want the event and the call stack which consumed the time.
This is why I prefer a trace to a video with a frame-rate counter. The video proves the hitch exists. The trace tells us what the machine was doing while everyone was swearing at it.
Nanite and Lumen are not free (but nor are they automatically guilty)
Nanite and Lumen are excellent pieces of technology. That sentence sometimes gets misread as “turn them on and stop thinking about performance”. They still have costs, and those costs depend rather heavily on the content and the target.
I use the GPU capture here and look at the expensive passes rather than toggling features at random. Lumen reflections, screen probes, virtual shadow maps, translucent effects and Nanite raster work can all become important. Which one matters is visible in the capture.
Feature toggles are useful as experiments. If disabling a feature produces a large, repeatable change, I have learned something. It does not necessarily mean the feature must be removed; perhaps its quality, update rate, content usage or screen coverage can be changed. The toggle is a measuring instrument, not the design decision.
Memory behaving like a frame-rate problem
A build can appear healthy for a while, then become ragged as more of the game is visited. It may hitch during camera turns, recover, and then degrade again. This often sends people looking for one bad frame when the more useful question is what has accumulated.
Texture streaming churn, repeated eviction and reload, pools sitting on the edge, or a large amount of poorly attributed memory can all turn into visible performance trouble. I look at memory over time, not only at the point of failure. A capture taken immediately after loading the level may miss the story completely.
“Other” and “unknown” are not harmless categories in a memory report. They are invitations to add tags and ownership until somebody can explain them.
A thousand perfectly reasonable ticks
Blueprint is not inherently slow. That is worth saying because “rewrite it in C++” has ended more than one discussion prematurely. A small amount of straightforward Blueprint work can be entirely sensible.
A thousand actors all ticking, querying, allocating or updating animation every frame is a different matter. The same is true in C++ - it merely takes longer before anybody becomes suspicious.
In Insights I look for repeated small costs which become large in aggregate: animation update and evaluation, skeletal meshes which need not update at full rate, AI and gameplay objects ticking off screen, components polling for a state change which could have been an event. Individually they look beneath notice. Multiplied by the population of a busy level, they own the frame.
This is also where a quiet test map can mislead. Profile the level with the crowd, combat, effects and UI actually present. An empty corridor is admirably fast and not usually the shipping workload.
And sometimes it really is the GPU
Eventually we reach the pleasingly direct case: the GPU number is the longest, it responds to resolution, and the capture contains one or more expensive passes.
Now the usual work becomes worthwhile. Look at overdraw, translucency, post-processing, shadow cost, material complexity, lighting and effects. Check how the result changes across the game rather than tuning one photogenic frame. Dynamic resolution can be part of a sound plan, but it should be recovering from understood, bounded GPU load - not disguising an unexplained 40ms scene.
I would still rank the costs before touching anything. A capture might contain twenty mildly regrettable things and one pass which is six milliseconds over budget. Fixing the twenty is satisfying. Fixing the six ships the game.
What I would do with a week
On the first day I would produce a small set of repeatable captures on target hardware: an ordinary scene, a bad scene and a transition which hitches. I would also make sure everyone agrees on the target frame time and test configuration. This sounds obvious; it is surprisingly easy to compare numbers from different builds and quality settings.
On day two I would split the trouble into CPU, GPU, waiting, memory and hitches. By then we should have stopped saying “performance” as though it were one system.
The middle of the week is for chasing the largest unexplained cost far enough to reproduce it, name its owner and try one cheap experiment. Near the end I would estimate the likely savings and the cost of each plausible fix. Then I would ask the people closest to the systems to argue with the ranking. They will know where my tidy theory collides with production reality.
The useful output is not a document containing every optimisation anyone can imagine. It is a short queue: this is costing roughly this much, here is the evidence, here is what we will try, and here is how we will know whether it worked.
The extra cause: nobody owns the budget
If performance is checked only before a milestone, the team gets a fresh detective story every few months. The same regressions return because nothing notices them arriving.
A target-hardware run in the nightly or regular build, with a few representative scenes and visible frame-time history, is not glamorous. It is, however, much cheaper than another emergency optimisation month. Give somebody authority to challenge a regression while the change is still fresh.
And when the build next says 24, resist the urge to vote on the cause. Take the trace first.
A performance review can include target-hardware builds, captures and trace analysis. If you only need to decide what evidence to gather, an advisory session is the lighter starting point.