Usually the query, not the tool: too fine a grain pulled to the client, a live query where an extract would do, or unfiltered scans. Pre-aggregate in the warehouse to the grain the dashboard actually shows, push filters down, and cache or extract where freshness allows. Row-level detail behind a summary should load on demand, not upfront.
How to answer it
The query, almost always. The tool renders a few hundred numbers in milliseconds; what takes 30 seconds is fetching a few hundred million rows to compute them.
Look in this order:
What the dashboard actually asks for. Open the query the tool generates. If it pulls row-level data and aggregates in the browser, or pulls a year to show a week, that is the problem.
The grain. A dashboard that shows daily totals should read a table that has daily totals. Pre-aggregate in the warehouse to the grain displayed; row-level detail loads on demand behind a click.
Filters pushed down. A date filter applied in the tool after the query returned everything is a full scan every time. Make the filter part of the query, on a partitioned column.
Live versus cached. If the data refreshes nightly, the dashboard should read an extract or a cached result, not hit the warehouse on every open. Match the cache lifetime to the refresh cadence.
The number of queries. Twenty tiles that each run their own query against the fact table is twenty scans. One summary table, twenty cheap reads.
Only then the tool: rendering thousands of marks, a slow custom visual, or a calculation that runs per row in the client.
Worked line: "The executive dashboard ran 14 live queries against a 2 billion-row fact table on open. A nightly mart at the daily-by-region grain has 40,000 rows; the dashboard reads that, loads in under two seconds, and the detail page queries the fact table only when someone drills."
What they are checking: grain and pre-aggregation, filter pushdown, and cache matched to refresh.
Common mistake: blaming the BI tool and buying a faster plan for it.