On this page
The short answer
You open the report. You click a slicer. Then you wait. The spinner turns for five, ten, fifteen seconds before a single chart redraws. People have quietly stopped opening it, and someone has started asking whether Power BI was the wrong choice.
Here is the short answer. Power BI is rarely slow because of the visuals. It is slow because of the data model underneath them. A visual is only a request. It asks the engine a question, and the engine has to answer that question every time you click. If the model is shaped badly, every one of those questions is expensive, and no amount of tidying the report page will make them cheap.
The engine that answers those questions is called VertiPaq. It is a columnar store, which means it keeps each column of data together rather than each row. That design is very fast when the model is shaped for it, and surprisingly slow when it is not. So the real question is not why Power BI is slow. It is what your model is asking the engine to do.
What this usually looks like
It usually shows up once the report has been live for a while. It felt quick in the demo. Now there is a year of data in it, three new pages, and a wall of slicers, and it drags.
The pattern is familiar. One giant table holds everything: every order, with the customer name, the product description, the sales rep, the region and the full address repeated on every row. Slicers sit on high-detail fields such as email address or transaction reference. A single page carries fifteen or twenty visuals, several of them cross-filtering each other. The refresh takes a little longer each week, and one morning it fails.
People reach for the visible things first. They swap chart types, cut the number of colours, or move visuals around the page. It helps a little, briefly. Then the report is slow again, because the cause was never on the page. It was in the table feeding it.
Before you touch a single visual
You can find most of the cost yourself in an afternoon, without rebuilding anything. The goal is not to guess. It is to measure where the time actually goes, and to size the model honestly. Two free tools do almost all of the work. Performance Analyzer is built into Power BI Desktop and times every visual on a page. DAX Studio is a free companion tool that shows query timings and how much memory each column uses.
Seven readings, and you will usually know the cause before you have touched a single visual.
What the result tells you
Each reading points at a different cause, and each cause has a different fix. Read yours like this.
- DAX-heavy visuals. The time is spent answering the question, not drawing it. Likely cause: a costly measure, a calculated column doing work at query time, or a model that forces the engine to scan too much.
- Render-heavy pages. The engine answers quickly, but too many visuals fire at once. Likely cause: fifteen or more visuals on one page, each a separate query.
- One very large column. A single column dominates the model size. Likely cause: high cardinality, meaning the column holds a very large number of distinct values, which the columnar engine cannot compress well.
- One flat table. There are no dimensions, only a wide table with everything repeated. Likely cause: the source was loaded as it came, without modelling.
- Slicers on unique fields. A slicer is built on a near-unique column. Likely cause: the field has thousands or millions of distinct values, so the slicer is expensive to build and to filter.
- A slow or failing refresh. The refresh reloads all history every time. Likely cause: no incremental refresh, so years of unchanged data are reloaded on every run.
The most common finding by far is the flat table with one or two enormous columns. Fix that, and most of the other symptoms ease at the same time.
What is actually slowing it down
Underneath the report, a handful of technical ideas explain almost all of it.
Compression is where the speed comes from. Power BI stores imported data in VertiPaq, the columnar engine named earlier. Because it keeps each column together, it can compress a column hard when that column repeats itself. A country column with a dozen distinct values compresses to almost nothing: the engine stores each distinct value once and points to it. This is why the shape of your columns matters more than the number of rows.
Cardinality is the number of distinct values in a column. A column with few distinct values is low cardinality, and it compresses well. A column with almost as many distinct values as rows, such as an order ID, an email address, or a timestamp recorded to the second, is high cardinality, and it barely compresses at all. High-cardinality columns are usually the single largest thing in a slow model. The fix is often to remove what you do not need, or to reduce precision. Split a datetime into a date and a separate time if you do not need per-second detail. Drop the free-text field nobody filters on.
A star schema is the shape the engine wants. Instead of one wide table with everything repeated, you split the data into facts and dimensions. A fact table holds the events you measure, such as one row per order line, with keys and numbers only. Dimension tables hold the descriptive detail, such as one row per customer or per product, joined back to the facts by a key. The name comes from the shape: a central fact table with dimensions around it like the points of a star. This removes the repetition, shrinks the model, and gives the engine simple joins to follow.
Relationships connect those tables, and each relationship has a cardinality that describes how many rows on one side match rows on the other. The healthy default is one-to-many: one customer row to many order rows. Many-to-many relationships are slower and easier to get wrong, and a relationship built on a high-cardinality key costs more to resolve. Keep the keys you join on small and clean.
DAX is the formula language you write calculations in, and there are two places to put them. A calculated column is worked out row by row when the data refreshes, and then stored, taking space in the model, often at high cardinality. A measure is worked out at query time, only for the cells currently on screen, and stores nothing. As a rule, a value you aggregate, such as a total or a ratio, should be a measure rather than a calculated column.
-- Costly: a calculated column, stored on every row, then averaged in a visual Margin % = -- calculated column DIVIDE ( Sales[Profit], Sales[Sales] ) -- one value per row, high cardinality -- Better: a measure, computed only for the cells on screen Margin % := -- measure DIVIDE ( SUM ( Sales[Profit] ), SUM ( Sales[Sales] ) )
The first version creates a stored value on every row, adds a high-cardinality column to the model, and gives the wrong answer once a visual averages it (the average of many ratios is not the ratio of the totals). The second computes nothing until you look at it, stores nothing, and returns the correct figure at whatever level the visual asks for. The same result on paper, at a very different cost.
Storage mode decides where the data lives when a visual asks a question. Import mode loads a copy of the data into VertiPaq, in memory, and is the fastest option for most reports. DirectQuery leaves the data in the source and sends a query to it on every interaction, so the report is only ever as fast as the source and the connection. It is the right choice when the data is too large to import or must be live to the second, and the wrong choice when it is picked out of habit. A composite model mixes the two, importing small dimensions while querying a large fact table live. If your report is slow and set to DirectQuery, ask first whether it truly needs to be.
Incremental refresh controls how much data reloads each time. Without it, every refresh reloads all history, including years of rows that have not changed. With it, Power BI reloads only the recent window, such as the last few days, and leaves settled history in place. On a large fact table this is the difference between a refresh that takes minutes and one that takes hours or fails.
Finally, some slowness is not inside Power BI at all. If the model reads from a database view that is itself slow, the report inherits that cost on every refresh, and under DirectQuery on every click. It is worth pushing heavy shaping upstream into the source or a warehouse, so Power BI receives data that is already modelled. Deciding whether that upstream layer should be Power BI, a warehouse, or both is a separate question, and worth answering deliberately.
What good looks like
Good does not mean the report has fewer features. It means the model is shaped so the engine barely has to work.
- A star schema, not a flat table. Facts hold the numbers, dimensions hold the descriptions, and nothing important is repeated on every row.
- Low-cardinality columns wherever possible. Unused columns removed, precision reduced where the detail is not needed, and no free text loaded just because it happened to be in the source.
- Aggregations written as measures, not stored as calculated columns, so nothing is computed until a visual asks for it.
- Import mode by default, with DirectQuery used only where it earns its place, and incremental refresh switched on for the large fact tables.
- Pages that ask fewer questions. A handful of focused visuals rather than twenty, and slicers built on small, tidy fields.
When the model is right, the report feels instant, and it stays fast as the data grows. Speed stops being something you chase and becomes a property of the design. One caveat is worth stating plainly. Fast and correct are different goals, and a quick report built on figures nobody trusts is not a win. It is worth confirming whether the underlying data is reliable alongside making it fast.
Common ways this goes wrong
- Optimising the visuals first. Changing chart types and colours treats a symptom. The cost is almost always in the model, not on the page.
- Loading the source table as it comes. Importing one wide table with everything repeated is the most common cause of a slow model, and the easiest to avoid.
- Keeping columns you never use. Every column costs memory, and the high-cardinality ones cost the most. If nothing filters or displays it, drop it.
- Using calculated columns for things that should be measures. It inflates the model and can give the wrong answer when aggregated.
- Reaching for DirectQuery by default. It is the right tool for specific cases, and a common cause of slowness when chosen by habit.
- Piling visuals onto one page. Each visual is a separate query, so twenty visuals is twenty questions on every click.
- Never setting up incremental refresh. Reloading all history on every run makes the refresh slow and fragile as the data grows.
When it is the model, not the report
The afternoon of measuring and tidying is worth doing, and often it is enough. It stops being enough when the problem is structural rather than cosmetic. At that point you are no longer speeding up a report. You are re-modelling the data underneath it, and possibly moving work upstream, and that is a build.
A performance decision guide
Once you know where the time goes, the first move is usually clear. Use this as a quick lookup.
| Symptom | Likely cause | First move |
|---|---|---|
| Slow on every click | DAX-heavy visuals or a costly model | Time the visuals in Performance Analyzer, then fix the model or the measure |
| The model file is huge | High-cardinality columns | List columns by size in DAX Studio; drop or reduce the largest |
| One wide table | No star schema | Split it into a fact table with dimensions |
| Slicers feel heavy | Slicers on unique fields | Move slicers to low-cardinality fields |
| The whole page lags | Too many visuals | Reduce visuals per page; split into focused pages |
| Refresh slow or failing | No incremental refresh | Turn on incremental refresh for the large fact tables |
| Only slow when live | DirectQuery on a slow source | Import if you can, otherwise optimise the source |
If most of your gap is the model rather than the page, that is good news. A model problem has a known set of fixes, and it stays fixed once the shape is right.