Most Power BI performance work starts with a guess. Someone read that calculated columns are bad, removes a few, and the report is still slow — because the actual cost was somewhere else entirely.
The workflow below takes about an hour on a model you have never seen and tells you where the time genuinely goes. All three tools are free.
Measure Before You Change Anything
All three are free, and none of them require access to your production tenant. A copy of the .pbix is enough to run the entire workflow, which means you can start today without waiting on an access request.
Record a baseline first, for a practical reason: query results cache. Run a visual twice and the second run is faster regardless of what you did in between, which makes it very easy to convince yourself a change worked.
Clear the cache between every measurement. DAX Studio has a Clear Cache command; use it before each run, and take three readings rather than one.
Step 1: Performance Analyzer, in Desktop
Built into Power BI Desktop under the View ribbon. Start recording, refresh the page, stop. You get a breakdown per visual, split three ways.
| Component | What it means | What to do |
|---|---|---|
| DAX query | Time the engine spent answering | The number that matters. Over 1,000ms needs attention. |
| Visual display | Time rendering the result | High usually means too many data points, not a model problem. |
| Other | Waiting, cross-filtering, overhead | High values often point at bidirectional relationships. |
Sort by DAX query descending. The top two or three visuals almost always account for most of the page load, and they are frequently not the ones users complain about.
Step 2: DAX Studio, for the Expensive Query
Copy the query from Performance Analyzer into DAX Studio, enable Server Timings, clear the cache and run it. The split you get is the important part.
What Server Timings tells you
Total 2,840 ms
SE 320 ms (11%) Storage Engine
FE 2,520 ms (89%) Formula Engine
SE Queries 14
Read it like this:
FE-heavy -> the DAX is the problem.
Row-by-row evaluation, iterators over
large tables, repeated sub-expressions.
SE-heavy -> the model is the problem.
Too much data scanned, poor compression,
high cardinality, missing relationships.
Many SE queries -> the engine is making repeated
round trips. Often a sign of a measure
that could be restructured.
This single split decides where you spend your effort. Rewriting DAX on an SE-heavy query is wasted work, and restructuring a model to fix an FE-heavy one is a great deal of effort for very little.
Step 3: VertiPaq Analyzer, for the Model Itself
Also in DAX Studio, under Advanced. It reads the model's internal statistics and shows you what is actually consuming memory, column by column.
What to look for, in order
1. Sort columns by Size, descending.
The top five usually account for most of the model.
2. Check Cardinality on those columns.
High cardinality compresses badly. Timestamps to
the second, GUIDs and free text are the usual
offenders.
3. Look at the Type column.
A calculated column in the top five is a finding.
It is stored, permanent, and not filter-aware.
4. Compare Dictionary vs Data size.
A large dictionary means many distinct values.
Splitting a datetime into date + time columns
frequently cuts this dramatically.
5. Check for columns you do not use at all.
Every loaded column costs memory whether or not
a report references it.
Point four is the one people miss. A single datetime column with second-level precision on forty million rows can be the largest object in the model. Splitting it into a date key and a separate time column often reduces it by an order of magnitude, because the date has a few thousand distinct values and the time has 86,400 — rather than the two together having tens of millions.
Reading the Three Together
| Pattern | Most likely cause | Where to start |
|---|---|---|
| FE-heavy, model is small | Inefficient DAX | Rewrite the top two measures |
| SE-heavy, model is large | Structure or cardinality | Split datetimes, remove unused columns |
| SE-heavy, model is small | Missing or wrong relationships | Check the model diagram |
| High 'Other' time across visuals | Bidirectional relationships | Replace with CROSSFILTER in the measure |
| One visual dominates everything | A single expensive measure | Isolate and rewrite it |
| Everything uniformly slow | Capacity or gateway | Check refresh history and capacity metrics |
That last row is worth stating plainly: sometimes the answer genuinely is capacity. But it is the last conclusion to reach, not the first, and you should only reach it after the first three tools have ruled out the alternatives.
One more reading worth knowing: a high SE query count with low total SE time. That means the engine is making many small round trips rather than one large scan, which usually points at a measure the engine cannot fold into a single storage query. Restructuring that measure often collapses fourteen queries into one.
And if Performance Analyzer shows large Other time spread evenly across every visual on a page, stop looking at individual measures. That pattern is almost always model-wide: bidirectional relationships, or a many-to-many relationship the engine has to resolve before it can answer anything.
Write Down What You Found
The step almost everyone skips, and the one that determines whether the work has any value beyond this week.
Record four things for every model you diagnose: the baseline timings, the FE/SE split for the worst query, the top five columns by size, and what you changed with the measured effect of each change. That takes twenty minutes and it turns a fix into knowledge.
Without it, the next person who inherits the model repeats the entire archaeology from scratch. Frequently that next person is you, eighteen months later, having forgotten which of the six things you tried was the one that worked.
It also settles arguments. When somebody asks in three months why a measure is written the unusual way it is, the answer is a recorded before-and-after rather than a recollection.
Four Mistakes People Make While Measuring
Measuring a warm cache. The most common one. Run a visual twice and the second is faster regardless of what you changed. Clear the cache before every single reading, and take three.
Testing against a subset. A model that performs acceptably against a development extract can behave completely differently against three years of production history. Cardinality, not row count, is what changes — and a subset usually has much lower cardinality than the real thing.
Optimising what users complain about rather than what costs the most. These are frequently different. Users complain about the page they open every morning; the expensive query is often on a page nobody mentioned, quietly consuming capacity that everything else needs.
Changing several things at once. If you remove three calculated columns, rewrite two measures and change a relationship, and the report gets faster, you have learned nothing transferable. Change one thing, measure, record. It is slower and it is the only way to build knowledge you can reuse on the next model.
What We Would Do
This workflow is what a performance audit consists of, done systematically and written up. We record baseline timings, run all three tools, and hand back findings ranked by impact against effort — with the before-and-after measurement for each change we make.
The deliverable most clients value is not the speed improvement. It is the documentation, because it means the next person who inherits the model does not repeat the archaeology.
If you would rather not spend the hour, that hour is the first day of a performance audit — a fixed-price two-week engagement ending in a documented, measured result. Or run the free 32-point checklist to see where the obvious gaps are first.
