BlogBusiness Intelligence

Power BI Performance Optimization: A Practical Guide

Eric Chen
Eric Chen
Senior BI Solutions Architect
·May 24, 202610 min read

Slow Power BI reports are almost always fixable. The causes — oversized data models, inefficient DAX, wrong storage mode, too many visuals — follow predictable patterns. Here is the diagnostic framework we use and the fixes that produce the biggest performance gains.

The quick answer

Slow Power BI reports almost always trace back to one of five root causes: a data model that is too large and unoptimised, DAX measures that are inefficient, the wrong storage mode for the workload, too many visuals on a single report page, or a misconfigured data gateway. The fix for each is different, and the diagnostic process matters — applying the wrong fix to a performance problem at best wastes time and at worst makes it worse. This is the diagnostic framework we use, starting from the highest-impact areas.

Start with Performance Analyzer

Before changing anything, measure. Power BI Desktop's Performance Analyzer (View → Performance Analyzer → Start Recording) captures the time each visual takes to render, broken down into three categories:

**DAX query time** — time spent executing the DAX query that retrieves data for the visual. This is the most common bottleneck for complex reports.

**Visual display time** — time spent rendering the visual after the data is returned. High visual display time indicates too many data points being rendered or an overly complex visual type.

**Other** — time spent on everything else: data model loading, report rendering overhead, gateway latency.

Run Performance Analyzer on your slowest report page and record the results for each visual. The visuals with the highest DAX query time are where optimisation effort pays off most. Sort by DAX query time descending and start at the top.

Fix 1: Optimise your data model

The single biggest performance lever in Power BI is the data model — its size, column cardinality, and relationship structure. Every visual in a Power BI report ultimately queries the in-memory data model (VertiPaq engine for Import mode). A poorly structured model queries slowly regardless of how well the DAX is written.

**Remove unused columns.** Every column in the model consumes memory and increases query scan time — even columns that no visual uses. Audit your model in Power BI Desktop's Model view and remove columns that no measures, relationships, or visuals reference. This is particularly impactful when importing from wide tables (Salesforce exports, ERP tables with 200+ columns) where most columns are irrelevant to the reporting use case.

**Reduce cardinality in text columns.** High-cardinality text columns (free-text descriptions, URL fields, long address strings) are expensive to compress and scan. Replace free-text fields with categorised lookup values where possible. For columns that must remain high-cardinality (customer names, product descriptions), ensure they are not used in relationships or as filter dimensions — use a lower-cardinality surrogate key for relationships instead.

**Avoid calculated columns where measures will do.** Calculated columns are computed at data refresh and stored in the model — they consume memory at all times. Measures are computed at query time and consume memory only while executing. If a calculation can be expressed as a measure (aggregation, ratio, conditional logic), use a measure. Use calculated columns only when you need a value available as a filter, slicer, or relationship field.

**Use integer surrogate keys for relationships.** Relationships between tables are faster when the join key is an integer rather than a string. If your fact table joins to dimension tables on a string key (customer name, product code), add an integer surrogate key and use that for the relationship. The storage and query performance improvement is significant at scale.

**Disable Auto Date/Time.** By default, Power BI creates hidden date hierarchy tables for every date column in the model. In a model with many date columns, these hidden tables can be as large as the primary data. Disable Auto Date/Time (File → Options → Data Load → uncheck Auto Date/Time) and use a single shared date dimension table instead.

Fix 2: Write efficient DAX

DAX performance is the most common bottleneck for complex Power BI reports with calculated measures. Inefficient DAX produces slow DAX query times that no data model or gateway optimisation will fix.

**Use variables (VAR) to avoid repeated evaluation.** When a DAX measure calls the same sub-expression multiple times, Power BI evaluates it multiple times. Using VAR to store the sub-expression result means it is evaluated once. Slow pattern: Margin % = DIVIDE(CALCULATE([Sales], ...) - [Cost], CALCULATE([Sales], ...)) — this evaluates CALCULATE([Sales], ...) twice. Fast pattern: declare VAR TotalSales = CALCULATE([Sales], ...) then RETURN DIVIDE(TotalSales - [Cost], TotalSales) — evaluated once, used twice.

**Avoid row-by-row iteration with SUMX/AVERAGEX where CALCULATE will do.** Iterator functions (SUMX, AVERAGEX, COUNTX) process the table row by row. For simple aggregations where CALCULATE can be used instead, CALCULATE is significantly faster because it operates at the aggregated engine level (VertiPaq direct query) rather than row-by-row.

**Minimise filter context manipulation.** CALCULATE with complex filter arguments is expensive. Each FILTER() call inside CALCULATE creates a row-by-row iteration. Replace FILTER(ALL(Table), condition) with a direct column filter where possible. Slow pattern: CALCULATE([Sales], FILTER(ALL(Products), Products[Category] = "Electronics")). Fast pattern: CALCULATE([Sales], Products[Category] = "Electronics"). The direct column filter version runs at storage engine speed; the FILTER version runs row by row.

**Use SELECTEDVALUE instead of VALUES for slicer reads.** When reading a slicer selection, SELECTEDVALUE is optimised for the single-selection case. VALUES returns a table and is slower when only a scalar is needed.

**Profile slow measures with DAX Studio.** DAX Studio is a free tool that connects to Power BI Desktop and provides query execution plans, storage engine query traces, and execution time breakdowns. For complex DAX that Performance Analyzer shows as slow, DAX Studio identifies exactly which part of the calculation is taking time.

Fix 3: Use the right storage mode

Power BI has three storage modes that determine how and when data is loaded into memory. Using the wrong mode for a given table is a common performance mistake.

**Import mode** — data is loaded into Power BI's in-memory VertiPaq engine at refresh time. Query performance is fast because all data is in-memory. Limitations: data size is bounded by available memory (Power BI Service has dataset size limits), and data is only as fresh as the last refresh.

**DirectQuery mode** — queries are sent to the underlying data source at report render time. Enables real-time data (no refresh lag) and handles very large datasets that cannot fit in memory. Limitations: every visual interaction sends a query to the source, so performance depends entirely on source query performance. Slow source queries produce slow visuals.

**Dual mode** — a hybrid: the table is cached in Import mode for queries that can be satisfied from the cache, and falls back to DirectQuery for queries that cannot. Useful for dimension tables that join to large DirectQuery fact tables.

The most common storage mode mistake: putting high-cardinality fact tables in Import mode when the data volume exceeds what VertiPaq can handle efficiently, or putting dimension tables in DirectQuery mode when they are small enough to cache. The right pattern for large datasets: Import mode for dimension tables (small, change infrequently), DirectQuery or Dual for large fact tables.

**Composite models** allow mixing Import and DirectQuery tables in the same model — dimension tables cached in Import mode, large fact tables in DirectQuery. This is the recommended approach for datasets that are too large for full Import but where fact table DirectQuery performance is acceptable.

Fix 4: Reduce visual complexity per page

Power BI reports with many visuals on a single page are slow because each visual executes at least one DAX query when the page loads, and visuals with cross-filtering relationships re-query when other visuals are interacted with. A page with 20 visuals executes 20 queries on load plus additional queries for every interaction.

**Limit visuals per page to 8–12.** For reports that currently have 20+ visuals on a page, split content across multiple pages with clear navigation. Each page load queries only the visuals on that page.

**Disable cross-filtering on visuals where it is not needed.** By default, visuals in Power BI cross-filter each other. Each cross-filter interaction triggers re-queries on all related visuals. For visuals that are informational (not intended to be used as filters), disable cross-filtering (Format → Edit Interactions → set to None for the relevant pairs).

**Use bookmarks for show/hide instead of conditional formatting.** Conditionally showing or hiding visuals based on slicer state is a common requirement. Implementing this with complex DAX measures and conditional formatting creates hidden visuals that still execute queries. Implementing it with bookmarks (show/hide states) means hidden visuals do not execute queries when hidden.

**Replace map visuals with simpler alternatives where possible.** Filled maps and ArcGIS maps in Power BI are among the slowest visual types because they load external map tiles and geocoding data. For reporting that does not require geographic exploration, bar charts or tables with regional breakdowns are faster and more readable.

Fix 5: Fix gateway and connectivity

For reports that connect to on-premise data sources (SQL Server, Oracle, file shares) through the Power BI On-Premises Data Gateway, gateway configuration is a common performance bottleneck — particularly for DirectQuery and scheduled refresh workloads.

**Run the gateway on dedicated hardware.** The gateway machine processes all queries between Power BI Service and on-premise data sources. If the gateway is running on a shared server alongside other workloads (application servers, database servers), it competes for CPU and memory. A dedicated gateway machine with sufficient resources (8+ cores, 16GB+ RAM for production workloads) eliminates resource contention.

**Scale out the gateway cluster for high concurrency.** If multiple reports are querying through the gateway simultaneously (peak usage periods, report subscriptions running at the same time), a single gateway node becomes the concurrency bottleneck. Gateway clusters (multiple nodes behind the same logical gateway) distribute query load across nodes. Add nodes when gateway CPU utilisation consistently exceeds 70% during peak periods.

**Place the gateway close to the data source.** Network latency between the gateway and the data source contributes to query time, particularly for DirectQuery where queries execute on every visual interaction. The gateway should be in the same network segment as the data source — not across a WAN connection.

**Refresh schedules: stagger and reduce.** Scheduled refreshes are computationally expensive and compete with user query performance during execution. Stagger refresh schedules so multiple datasets do not refresh simultaneously. For datasets that do not need hourly freshness, reduce refresh frequency — fewer refreshes means less competition for gateway and source resources.

Fix 6: Incremental refresh for large datasets

For Import mode datasets that are slow to refresh because of data volume, incremental refresh allows Power BI to refresh only the data that has changed — new records and recently modified records — rather than replacing the full dataset on every run.

Incremental refresh is configured in Power BI Desktop using a date range parameter. You define a rolling window of historical data to retain (e.g., 3 years) and a refresh window of recent data to refresh on each run (e.g., the last 7 days). Only the refresh window is queried from the source on each scheduled refresh, reducing refresh time from hours to minutes for large datasets.

Incremental refresh requires a date column in the fact table. For datasets that are partitioned by date in the source (transactional data, event logs), incremental refresh almost always significantly reduces refresh duration.

FAQs

How do I know if slow performance is a DAX problem or a data model problem?

Performance Analyzer is the starting point: if DAX query time dominates, the issue is DAX or the data model (both are queried through DAX). To distinguish between DAX efficiency and model structure, open the slow measure in DAX Studio and examine the query plan. A large number of storage engine (SE) queries indicates a model structure problem (high cardinality, inefficient relationships). A large formula engine (FE) computation time indicates a DAX efficiency problem.

My report is fast in Desktop but slow in the Service. Why?

Performance in Power BI Desktop runs queries against a local VertiPaq instance on your machine. Performance in the Service depends on the capacity allocated to your workspace. If your workspace is on shared capacity (Power BI Pro), you are competing with other tenants for compute. If it is on Premium capacity, check whether the capacity is undersized for the workload — Premium capacities have defined CPU and memory limits. Upgrading capacity tier or moving to Premium Per User typically resolves Desktop/Service performance discrepancies that are not caused by model or DAX issues.

We have a DirectQuery report that is very slow. What should we fix first?

DirectQuery performance is almost entirely determined by the performance of the underlying source queries. Start by capturing the SQL queries that Power BI sends to the source (using SQL Server Extended Events, Snowflake Query History, or equivalent source profiling). If the source queries are slow, the fix is at the source: indexing, query optimisation, materialised views (pre-aggregated summary tables that Power BI queries instead of the raw fact table). If the source queries are fast but the report is still slow, investigate gateway configuration and concurrency.

What is the maximum dataset size for Power BI Import mode?

Power BI Pro has a 1GB dataset size limit. Power BI Premium (capacity-based) supports datasets up to 400GB. Power BI Premium Per User supports datasets up to 100GB. These limits apply to the compressed in-memory size of the dataset, which is typically 5–10x smaller than the source data size due to VertiPaq compression. A 10GB source table might compress to 1–2GB in the model. For datasets that exceed these limits, DirectQuery or Composite model is required.

Our Power BI consulting practice optimises Power BI environments at enterprise scale — data model redesign, DAX review, gateway configuration, and capacity planning. If your reports are consistently slow and internal optimisation attempts have not produced results, book a free 30-minute audit and we will diagnose the root cause directly.

Get your data architecture audit in 30 minutes.

A former Microsoft data architect audits your data foundation, identifies your top priorities, and sends you a written plan. Free. No pitch.

Book a Call →