Optimizing Canvas Apps: Fix Slow Dataverse Gallery Loads with Delegation
A troubleshooting guide for anyone watching a Power Apps gallery crawl through five thousand Dataverse rows: what delegation actually does, which formulas break it, what to rewrite, and how to prove the fix worked. It also covers the whole app that takes thirty seconds to open and the tables with hundreds of thousands of rows.
A Power Apps canvas gallery that crawls over five thousand Dataverse rows is almost always a delegation problem. Delegation means the filtering, sorting and counting happen in Dataverse and only the matching rows travel to the app. When a formula cannot be delegated, Power Apps pulls the first five hundred rows, evaluates locally, and gives you an answer that is both slow and wrong. The fix is to rewrite Filter, Search and Sort with delegable functions, stop pulling columns and related records you do not display, reduce the working set the gallery is asked to show, and prove it with Monitor.
Two notes before the detail. First, which functions delegate is decided per connector and Microsoft revises the list, so treat the function names below as the shape of the problem and confirm the current behaviour in the Power Fx delegation documentation and, more usefully, in Power Apps Studio itself. Studio marks every non delegable formula with a warning while you type, and that warning is the authoritative answer for your app on the day you are looking at it. Second, we do not publish load time benchmarks. How long a gallery takes depends on table width, how many related records each row touches, what else runs in OnStart, and the device the app is on, so any number quoted without seeing the app is decoration. What we can give you is the arithmetic on the row limits and the order in which to change things.
What is the short answer to a slow canvas app gallery?
Delegation decides where the work happens
A delegable formula is sent to Dataverse, which filters five thousand rows down to the twenty that match and returns those. A non delegable formula is evaluated inside the app, so Power Apps first has to download rows to evaluate against, and it will only ever download up to the data row limit. Every performance difference you are seeing follows from which of those two things your Items property is doing.
A slow gallery is usually also a wrong gallery
This is the part the speed question hides. If your filter is not delegated, it runs against the first five hundred rows only. Row four thousand nine hundred can match your criteria perfectly and never appear. Users report it as missing data months later, long after the app was signed off against a test table with two hundred rows in it. Fixing delegation is a correctness fix that happens to also make the app fast.
Raising the data row limit to two thousand is not the fix
It is the first thing everybody tries and it makes things worse in both directions. The app now downloads four times as much data on every screen, so it gets slower, and the correctness bug is still there, just hidden until the table passes two thousand rows. Set the limit back to five hundred while you diagnose. If lowering it changes your results, you have found a non delegable formula.
Why does the whole canvas app take 30 seconds to load?
When a canvas app takes thirty seconds to open, diagnose before rewriting. Run Monitor and record what fires in OnStart, then count and time every data call. Look for non delegable queries pulling whole tables, collections rebuilt on every screen, gallery templates with nested galleries and per row lookups, and reference data fetched repeatedly. Then fix in order of effort: defer everything the first screen does not need out of OnStart, load what remains with Concurrent or as named formulas, cache reference data once, flatten gallery templates, replace repeated lookups with a pre-joined collection, and move heavy aggregation to the server. Measure again after each change.
A slow gallery and a slow app often share causes, but the start up path has its own. These are the ones a Monitor trace of a cold start usually shows on an inherited app.
OnStart doing the work of every screen
Most slow apps grew their OnStart one requirement at a time: a collection for the settings screen, a lookup for the admin panel, a count for a dashboard tile nobody opens first. Everything in OnStart runs before the user sees anything, in sequence unless somebody wrapped it, so the start time is the sum of every screen the app has. The Monitor trace makes this obvious, because the calls line up one after another before the first screen renders.
Whole tables collected because delegation was confusing
The same ClearCollect pattern covered in the gallery causes above, but at app level: several tables collected at start so later screens can filter them locally. Each one is a full download capped at the data row limit, which makes start up slow and the data incomplete at the same time.
Collections rebuilt on every screen visit
OnVisible formulas that clear and rebuild a collection each time the user navigates back mean the app never finishes loading, it just reloads in smaller pieces. Reference data that changes once a month does not need to be fetched every time somebody returns to the home screen.
Heavy first screen templates
A home screen with a gallery whose row holds a nested gallery, several LookUp calls, conditional visibility formulas that each query data, and an image per row is effectively dozens of requests before the app feels usable. Nested galleries are the worst of these, because the inner gallery issues its own query for every outer row.
Navigation logic in OnStart
Apps that decide the first screen by running queries in OnStart and then calling Navigate force the whole start sequence to finish before that decision can be made. App.StartScreen expresses the same decision declaratively, and Microsoft recommends it over Navigate in OnStart; check the current documentation for how your app is configured.
How do you audit and fix a slow loading canvas app, step by step?
Steps 1 to 3 are the audit. Steps 4 to 9 are the fixes, ranked from least to most effort, and step 10 proves them. If the audit shows a slow gallery rather than a slow start, the gallery method further down this page is the one to follow.
Record a Monitor session from a cold start
Open the published app with Monitor attached, from a fresh session rather than a warm one, and let it reach the first usable screen. Export or note every call made before that point with its duration, the table it hit, and how many rows came back. This is your baseline, and it is the only evidence that will settle which part of the thirty seconds belongs to what.
List everything OnStart does and mark what the first screen needs
Copy the OnStart formula out and go through it line by line. For each collection, variable and lookup, write down which screen actually uses it. Anything the first screen does not need is a candidate to move, and on most inherited apps that is the majority of the formula.
Count the data calls and flag the ones pulling whole tables
In the Monitor trace, flag calls that return a number of rows equal to the data row limit, calls against the same table repeated in one session, and calls that are individually quick but fired dozens of times. The first group is a delegation problem, the second is missing caching, the third is a gallery template problem, and each gets a different fix below.
Defer non essential OnStart work to the screen that uses it
Move each item you marked as not needed at start into the OnVisible of the screen that uses it, guarded so it only loads if it has not already. This is the lowest effort change on the list and usually the largest. Replace any Navigate call in OnStart with an App.StartScreen formula so the first screen is not waiting on the start sequence.
Load what must stay in parallel, or as named formulas
Wrap independent loads that genuinely must happen at start in Concurrent so they run side by side rather than one after another. For values that are really definitions rather than steps, such as the current user record or a filtered reference table, consider named formulas in App.Formulas, which are evaluated when first used rather than all at start. Named formulas are a newer feature, so check the current Microsoft documentation for their status and limits in your environment.
Cache small reference data once per session
Option lists, configuration rows, a list of sites or product categories: load each once, keep it in a collection or named formula, and stop rebuilding it in OnVisible. Only small and slow changing data belongs here. Large transactional tables stay queried at the source, for the delegation and staleness reasons covered in the gallery section of this page.
Flatten the gallery templates on the first screens
Remove nested galleries from rows where you can, move images and secondary detail to the detail screen, reduce the number of controls per row, and replace visibility formulas that query data with values already on the row. Every control in a template is multiplied by the number of rows rendered.
Replace repeated lookups with a pre-joined collection
Where a screen needs a related value per row and the related set is small, fetch that set once and join it to the small working set before the gallery renders, so each row reads from memory instead of issuing its own LookUp. Where the related set is large, denormalise the value onto the table on the server instead, as described in the section on queries that cannot be delegated.
Move heavy aggregation out of the app
Counts, totals and grouped figures shown on a landing screen should be read from a precomputed row, a rollup column or a report, not calculated in Power Fx over a downloaded table. If the app is computing a dashboard at start, it will always be slow at volume and the numbers will be wrong past the row limit.
Measure again against the same baseline
Repeat the cold start Monitor session on the same device and network and compare time to first usable screen, number of calls before it, and rows returned. Make one change at a time where you can, so you know which fix earned its place, and test with production data volumes rather than a development copy.
When the start up formula is slow because the app itself has become hard to change, with logic copied across screens and nobody sure what depends on what, the performance work and the structural work are the same job. Extracting shared logic into named formulas and components safely is covered in our guide to refactoring a tangled Power Apps solution without a rewrite.
Why is the gallery slow, and why is it rarely only delegation?
Delegation is usually involved, and on most apps we look at it is not the only thing costing time. Fixing the Filter formula and nothing else typically removes part of the wait and leaves the user still describing the app as slow. These are the causes worth checking, in the order they tend to matter.
A non delegable predicate in the Items property
The direct cause and the one Studio will tell you about. Something in the Filter or Search formula cannot be translated into a Dataverse query, so the whole formula stops being delegated. It is rarely the function you suspect. A single text manipulation, a comparison against a column of a related record, or an expression on the left hand side of the comparison is enough to break delegation for the entire statement.
The whole table collected at app start
ClearCollect over a five thousand row table in OnStart is a pattern people adopt precisely because delegation was confusing, and it is the most expensive thing in most slow apps. The user waits through the entire download before the first screen appears, the collection is capped at the data row limit anyway, and it goes stale the moment somebody else edits a record. It replaces a delegation problem with a delegation problem plus a startup problem plus a staleness problem.
Lookups running once per visible row
A LookUp call inside a gallery template is evaluated for every row the gallery renders, so scrolling issues a request per row per lookup. Two of them in the same template doubles it. This is the classic reason a gallery feels fine on the first screen and then stutters continuously as the user scrolls, and it is invisible in the formula bar because each individual call is cheap.
Retrieving columns nobody displays
A wide table with a hundred columns costs a hundred columns of payload per row unless the app is told otherwise. Explicit column selection is the setting that limits the retrieve to the columns the app actually references, and on wide tables the difference in payload size is large. It is also easy to defeat by accident, by passing a whole record into a component or a variable in a way that makes the app assume every column is needed.
Sorting on something the server cannot sort on
Sort and SortByColumns delegate, but only over a column the data source can order by. Sort over a calculated expression, a concatenation of two columns, or a value from a related record is evaluated locally, which quietly turns a delegated query into a non delegated one even though the Filter around it looked correct.
A search box that queries on every keystroke
A text input wired straight into the gallery Items fires a query for every character typed. Seven characters is seven round trips, six of which are already obsolete before they return. The user experiences this as the whole app freezing while they type, and it is entirely separate from whether the query itself is delegable.
Showing five thousand rows in the first place
The honest one. Nobody scrolls five thousand rows. A gallery is a work queue, and a work queue that opens on everything is a design decision rather than a technical constraint. The apps that feel instant are the ones that open on the twenty rows assigned to the person looking at the screen, with search and filters to reach the rest. Perfect delegation over an unbounded set is still a worse experience than a bounded set.
Images and files rendered in the row template
Image columns, file columns and attachment thumbnails in a gallery row are a separate download per row, and they are not covered by anything in the delegation documentation. On a list view they are usually decoration. Moving them to the detail screen removes a class of network traffic that no formula rewrite would have touched.
What does the data row limit do, and why does raising it make things worse?
This section exists because raising the limit to two thousand is the most common response to the symptom and it is the wrong one in both directions. It is worth understanding exactly what the setting does, because used correctly it is the fastest diagnostic you have.
What the data row limit is
An app level setting, found under the general settings of the app, that caps how many rows Power Apps will retrieve for a non delegable operation. It defaults to five hundred and the maximum is two thousand. It has no effect at all on delegable queries, which is the detail that makes it such a good diagnostic: if changing this number changes what your gallery shows, something in your formula is not being delegated.
What happens when a formula is not delegated
Power Apps retrieves up to the limit, in the source order the connector returns, and applies your filter to that subset in the client. There is no error and no warning at runtime. The gallery shows a plausible, incomplete answer. On a five thousand row table with the default limit, you are filtering ten percent of your data and presenting the result as if it were all of it.
Why the maximum of two thousand is not a solution
Two thousand of five thousand is still forty percent. The app pays four times the download cost on every screen that touches the table, on whatever connection the user has, and the correctness problem returns the day the table grows. It buys you a slower app and a delayed bug. The only case where raising it is legitimate is a table you can guarantee will stay small, and guarantees like that tend not to survive a second department joining.
How a delegated gallery actually loads
When the query is delegable, the gallery does not download five thousand rows at all. It requests a page of rows, renders them, and requests the next page as the user scrolls. The row limit is irrelevant and the table can be far larger than it without anything changing. This is why a correctly delegated gallery over a large table can feel faster than a badly written one over a small table.
Where the warning appears
Power Apps Studio underlines the non delegable part of the formula and shows a delegation warning on the control. The warning names the specific function or operator that could not be delegated, which is the fastest route to the cause. Treat every one of them as a defect to be closed rather than a suggestion, and do not ship an app with open delegation warnings on any table that can grow.
The tool that proves it
Monitor, opened from Studio or against a published app, records every network call the app makes with its duration and its payload. It is the only honest measurement available, and it settles arguments quickly: you can see whether the filter went to the server, how many rows came back, how many separate requests one scroll produced, and which of them was actually slow. Change nothing until you have looked at it.
What delegates to Dataverse and what does not?
Organised by what you are trying to do rather than alphabetically, and with the workaround in every row, because the workaround is the part a function reference does not give you. Confirm anything critical against the current Microsoft delegation documentation and against the warning Studio shows in your own app.
| What you are doing | Delegated to Dataverse? | What to do about it |
|---|---|---|
| Filter and LookUp with simple comparisons | Delegable. Equals, not equals, greater than, less than, and the And, Or and Not operators combining them all translate into a Dataverse query. | This is the target shape. Keep the column on the left of the comparison and a plain value or a variable on the right, and the whole statement stays on the server. |
| StartsWith | Delegable against Dataverse text columns, which makes it the workhorse of any search box that has to scale. | Use it as the default search predicate. Most users searching a customer list are typing the beginning of a name, so the behaviour is usually what they expected anyway. |
| The in operator used as a substring test | Not delegable. This is the single most common cause of a slow gallery, because it reads naturally and it is what most people write first. | Replace it with StartsWith where the semantics allow. Where a genuine contains search is a hard requirement, use the Search function or move the query server side rather than accepting a client side scan. |
| Text functions such as Len, Left, Mid, Upper and Lower inside a filter | Not delegable. Wrapping a column in any of them makes the comparison a client side expression and breaks delegation for the entire formula. | Do the transformation before the filter runs, into a variable, or store the derived value in its own Dataverse column maintained on the server so the filter can compare against a plain column. |
| Filtering on a column of a related record | Not delegable in the general case. Walking from the row to its parent inside a Filter predicate is evaluated in the client. | Filter on a lookup column directly where you can, since comparing the lookup itself is a comparison against a column on the row. Otherwise denormalize the one value you filter on onto the child table and keep it current with a plugin or a flow. |
| Sort and SortByColumns | Delegable over a sortable column, not delegable over an expression. Sorting by two columns concatenated, or by anything computed, is done in the client. | Sort by a real column. If the ordering the business wants is genuinely composite, materialize it as a column on the table and sort by that. |
| CountRows, Sum, Average, Min and Max | Delegable against Dataverse within documented limits, so a count on a large table does not have to download the table. | Where an aggregate is not delegable, or where the number is expensive and shown on a dashboard, precompute it: a rollup column, a plugin maintained field, or a Power Automate flow writing a summary row. |
| AddColumns, GroupBy, Distinct and Ungroup | Not delegable. They are table shaping operations that run on whatever the app already has in memory, which is at most the data row limit. | Reduce the set with a delegable Filter first, then shape the small result. If the grouping is the point of the screen, do the grouping in Dataverse or in Power BI rather than in Power Fx. |
| ForAll over a data source | Not delegable as a query mechanism. Iterating a table to build a result set downloads the table and is slow at any meaningful volume. | Express the requirement as a filter. Where it genuinely cannot be, move the loop server side into a flow or a custom API and call it once from the app. |
| Collect and ClearCollect over a whole table | Capped by the data row limit, so a collection built from a five thousand row table silently contains five hundred rows. | Collect only small, stable reference data. Point galleries at the data source directly so paging and delegation can do their work, and accept that a cached copy of a large table is a bug rather than an optimisation. |
How do you fix a slow gallery, step by step?
Steps 1 to 3 find the cause. Steps 4 to 8 fix the query and the payload. Steps 9 and 10 fix the design, which is usually where the largest improvement is. Steps 11 and 12 handle what is left and prove it. The order matters, because rewriting formulas before you have a Monitor trace is optimising a number you have not measured.
Measure with Monitor before you change any formula
Open Monitor from Power Apps Studio, load the screen, and scroll the gallery. You will see every network call with its duration and the number of rows it returned. Read it before touching anything. It tells you whether the wait is one slow query, five hundred rows of payload you did not need, or a hundred small lookup calls fired by the row template, and those three findings lead to three different fixes.
Set the data row limit back to five hundred and see what changes
If somebody raised it to two thousand to make a symptom go away, put it back to five hundred in the app settings while you diagnose. If the gallery contents change when you do, you have proved that a formula is not being delegated and is filtering a subset. That is a faster diagnosis than reading the formula, and it also stops the raised limit from masking the next problem you introduce.
Close every delegation warning in the app, not just the one on the gallery
Studio marks non delegable formulas as you type and names the operator responsible. Work through all of them, including the ones on screens nobody has complained about yet, and record which function broke each formula. A single non delegable element makes the entire statement non delegable, so the fix is usually one operator rather than a rewrite.
Rewrite the filter so the column is on the left and a value is on the right
Delegation works on comparisons between a column and a value. Move every calculation, text manipulation and function call out of the predicate and into a variable computed before the Filter runs, then compare against that variable. This single mechanical change fixes a large share of real delegation warnings without altering what the formula means.
Replace the in operator with StartsWith in search predicates
A contains style search using the in operator is not delegated to Dataverse and is the most common cause of a slow gallery. StartsWith is delegated and matches what most users are doing when they type into a search box. Where a true contains search is genuinely required by the business, treat it as a server side problem rather than as a formula to be tuned.
Make the search box wait for the user to stop typing
Turn on delayed output on the text input so the gallery queries once when typing pauses instead of once per keystroke, and do not query at all until two or three characters have been entered. On a large table this removes most of the requests the app was making, and it costs nothing but a property change.
Remove LookUp calls from the gallery row template
A lookup inside the template runs per rendered row. Resolve the value another way: use the lookup column already on the row where the display name is enough, denormalize the field you need onto the table, or fetch the related set once into a small collection before the gallery renders and reference that. The measurable effect is on scrolling, which is where users judge an app as slow.
Turn on explicit column selection and stop retrieving unused columns
Confirm explicit column selection is enabled for the app so Dataverse returns only the columns the app references. Then check you are not defeating it by passing whole records into components or variables in a way that forces a full retrieve. On a wide table this reduces payload per row substantially and it requires no change to the query itself.
Bound the set the gallery opens on
Default the gallery to the rows that person needs right now, which is usually their own records, or open items, or this week, and let search and filters reach the rest. Five thousand rows in a scrolling list is not a requirement anybody asked for, it is what happens when nobody decided. This is the change that makes an app feel instant rather than merely correct.
Stop collecting the table at start up
Remove ClearCollect over large tables from OnStart and point the gallery at the data source so paging and delegation apply. Keep collections for small reference data only, load them with Concurrent so they run in parallel rather than in sequence, and move anything not needed for the first screen out of the start up path entirely.
Move genuinely non delegable work to the server
Some requirements cannot be expressed as a delegable Power Fx formula: a real contains search across several columns, a filter on an aggregate, a grouping over the whole table. Those are server side problems. The supported answers are a Dataverse column maintained by a plugin or a flow, a custom API called from the app, or a saved Dataverse view that already encodes the filter. Choosing one of those is the fix; raising the row limit is not.
Prove it at full volume and check a row past the limit
Test against a table with production row counts, not a development table with two hundred rows, because delegation defects are invisible below the limit. Deliberately search for a record you know sits well past row five hundred in source order and confirm it is found. Then run Monitor again and compare the call count and durations against the reading you took in step one, so the improvement is a measurement rather than an impression.
Which formulas should you rewrite, before and after?
Column and table names below are illustrative, so map them onto your own schema. The pattern in each row is the point: move the computation out of the predicate so what reaches Dataverse is a comparison between a column and a value.
Instead of
Filter(Accounts, TextInput1.Text in name)The in operator as a substring test is not delegated. Power Apps downloads up to the row limit and scans it in the client, so the gallery is slow and misses matches beyond the limit.
Write
Filter(Accounts, StartsWith(name, TextInput1.Text))Instead of
Filter(Contacts, Upper(lastname) = Upper(varSearch))Wrapping the column in a text function turns the comparison into a client side expression and breaks delegation for the whole statement.
Write
Filter(Contacts, lastname = varSearch) with the case handling done once when varSearch is set, or against a column stored in a consistent caseInstead of
Filter(Cases, Customer.Country = "Germany")Filtering on a column of a related record is evaluated in the client, because the related row is not part of the query being sent.
Write
Filter(Cases, CustomerCountry = "Germany") where CustomerCountry is a column on the case table kept current by a plugin or a flowInstead of
ClearCollect(colOrders, Orders) in OnStart, gallery Items set to colOrdersThe collection is capped at the data row limit, so it silently holds a fraction of the table, and the user waits for the whole download before the first screen renders.
Write
Gallery Items set to Filter(Orders, ownerid = User().Email && statuscode = varOpen), with the data source queried directly so paging appliesInstead of
SortByColumns(Filter(Accounts, statecode = 0), "name & city")Sorting by an expression rather than a column is done in the client, which pulls the filtered set down to sort it and undoes the benefit of the delegated filter.
Write
SortByColumns(Filter(Accounts, statecode = 0), "name") or a single sortable column that encodes the ordering the business wantsInstead of
CountRows(Filter(Invoices, Year(createdon) = 2026))Applying a function to the column inside the predicate breaks delegation, so the count is computed over whatever fits under the row limit and is simply wrong.
Write
CountRows(Filter(Invoices, createdon >= varYearStart && createdon < varYearEnd)) with both boundaries computed as variables firstInstead of
A text input bound directly to the gallery Items propertyEvery keystroke issues a query, so a seven character search produces seven round trips and the app appears to freeze while the user types.
Write
Delayed output enabled on the input, and the filter guarded so it only runs once a minimum number of characters has been enteredWhat if the query genuinely cannot be delegated?
Some requirements cannot be written as a delegable Power Fx formula, and pretending otherwise is how a non delegable filter ends up shipped with a comment next to it. These are the supported answers, each with what it costs you.
A column that holds the answer
The most common and the most boring fix. If you filter on something the app has to compute, compute it once on the server instead and store it: a denormalized value from the parent record, a status derived from three other fields, a normalized search key. A plugin or a Power Automate flow keeps it current, and the app filters on a plain column, which delegates. The cost is that you now own the logic that maintains it, so it belongs in a solution and under source control rather than in a note somebody kept.
A saved Dataverse view
Where the filter is fixed rather than driven by the user, a view already encodes it on the server, and pointing the app at the view moves the work to Dataverse without any Power Fx at all. This suits work queues and role based lists well, and it has the side benefit that the same definition is reused by the model driven app. It does not help when the filter has to respond to what the user types.
A custom API or a flow called once
For a genuine contains search across several columns, a query against an aggregate, or anything needing a real join, the honest answer is a server side call that returns the small result set the app displays. Called once with parameters rather than iterated per row, this is fast and fully correct. The cost is a component to build, test and version, which is the point at which the work stops being configuration.
A custom control for the list itself
Where the screen needs behaviour the gallery cannot express at volume, such as virtualized scrolling over a very large set, server side paging with a page size you control, or column level filtering in a grid, a custom control built with the PowerApps Component Framework is the supported route. It is a heavier answer than a formula rewrite and should be the last option, not the first.
Deciding the table is the wrong shape
Occasionally the right conclusion is that a screen listing five thousand active rows describes a process problem rather than a technical one. Rows that are all open because nothing closes them, or a single table doing the work of three, will produce slow screens no matter how well the formulas are written. That is worth naming before anyone spends a week on Power Fx.
If the answer turns out to be a custom control rather than a formula, the practical questions are whether a ready made component already does it and what building one involves, which we cover in using PCF controls in canvas apps and in the buy, build or customize decision guide. If the volume problem is really about getting the data into Dataverse rather than reading it back, that is a different exercise, covered in our Dataverse bulk import strategy.
What changes when the table has hundreds of thousands of rows?
The section above deals with one formula that cannot be delegated. At hundreds of thousands of rows the table itself becomes the design constraint, and the app has to be built so it never treats the table casually. These are the options, from the ones you can apply inside the current app to the point where the answer is a different architecture.
A delegation warning on a table this size is a guaranteed wrong answer
The data row limit tops out at two thousand. On a table with hundreds of thousands of rows, any non delegable formula is evaluating a tiny, arbitrary slice and presenting it as the whole. There is no tuning that rescues it. Every warning against a table of this size has to be closed or the requirement has to move to the server.
Server side paging and filtering, with a required filter
At this volume, never let a screen open on the unfiltered table even if the query delegates. Make at least one selective filter mandatory, such as owner, status, region or a date window, and let Dataverse page the results. Point fixed work queues at saved views so the filter lives on the server. Check that the columns you filter and sort on are ones Dataverse can use efficiently, and look at the query shape in Monitor rather than assuming it.
Summary tables maintained by a flow or plugin
Where a screen shows totals, counts per status or per region, or a leaderboard, keep a small summary table that a Power Automate flow or a plugin updates when the underlying records change, or on a schedule if near real time is not required. The app reads a few dozen summary rows instead of aggregating hundreds of thousands. This is different from the per row denormalised column above: it removes aggregation from the app entirely. Decide up front how the summary is rebuilt if it ever drifts.
Archive cold data out of the working table
Tables grow large mostly with records nobody works on: closed cases, completed orders, last year and the year before. Moving closed records older than an agreed window into an archive table, or using Dataverse long term data retention where it fits, keeps the working table small enough for the app to stay responsive. Retention options and their licensing change, so check current Microsoft documentation before designing around them, and agree reporting access to archived data with the business first.
Knowing when the volume means a different architecture
A canvas app is a good front end for working on a bounded set of records. If users genuinely need to search, slice and analyse across the whole history, that is a reporting or search requirement and belongs in Power BI, a model driven app view, or a custom API over a store built for it. If the app is really a high volume transactional system, the honest conversation is whether a canvas app is the right tool at all, or whether a model driven app or a custom built application fits better. Deciding that early is cheaper than a second round of formula tuning.
Summary tables, archiving jobs and custom APIs are server side components that need to be built, versioned and supported, which is the kind of work covered by our Power Platform consulting and development service. Where the conclusion is that the workload has outgrown a Power Apps front end and Microsoft licensing does not fit either, a custom built application is one of the options we would put on the table.
How does Solzet fix an app like this?
Canvas app performance work is part of our Power Platform delivery rather than a product we sell separately, so this is what it looks like when we do it.
We measure before we rewrite
A Monitor trace on the screen that is slow, at production volume, before any formula changes. It usually shows that the delegation warning everybody was looking at accounted for part of the wait, and that the row template, the start up sequence, or the payload size accounted for the rest. Rewriting formulas without that reading is how a week disappears into a change nobody can measure.
We treat delegation defects as correctness defects
Every open delegation warning on a table that can grow is a place where the app will one day show an incomplete answer without saying so. We close them rather than annotate them, and we test with a record deliberately placed past the row limit, because that is the test that would have caught it originally.
We are willing to say the screen is wrong
A gallery that opens on every row in the table is a design decision, and often nobody made it deliberately. Bounding the default set to what that person actually works on is frequently the largest single improvement available, and it costs less than the formula work it replaces.
We build the server side piece when the formula cannot win
A denormalized column maintained by a plugin, a custom API, a flow, or a PCF control for a list the gallery cannot serve at volume. These are things we build routinely as part of Power Platform delivery, so the decision to go server side is made on the requirement rather than on what the team happens to be able to build.
We ship it through proper solution lifecycle management
Separate development, test and production environments, changes made in unmanaged solutions and promoted as managed ones. A performance fix that exists only as an edit somebody made in the production app is a fix you will lose, and probably at the worst moment.
Certified engineers in one time zone
We deliver Dynamics 365 Customer Engagement and the Power Platform from a single hub in Yerevan, Armenia, at GMT+4, which overlaps Western European hours and reaches into the US morning. Our engineers hold Microsoft certifications including PL-200, PL-400 and PL-600.
The engagement models, the certifications and what a Power Platform developer actually covers are on our Power Platform and Power Apps development page. If you are still deciding whether this is work for your own team or for a developer at all, delegation is one of the walls named in our consultant versus do it yourself decision guide.
What do people ask about canvas app delegation?
Why is my Power Apps canvas gallery so slow with five thousand Dataverse records?
Usually because the formula in the Items property is not being delegated to Dataverse, so Power Apps downloads rows and filters them inside the app instead of asking Dataverse to filter. Since it will only download up to the data row limit, which defaults to five hundred, the gallery is both slow and incomplete. On real apps there are normally two or three other causes stacked on top: the whole table collected into a collection at start up, a LookUp inside the gallery row template that runs once per visible row, columns being retrieved that nothing on screen displays, and a search box that queries on every keystroke. Open Monitor and look at the actual network calls before changing any formula, because those four causes have four different fixes and only one of them is a delegation rewrite.
What is delegation in Power Apps, in plain terms?
Delegation is Power Apps deciding whether it can hand your filtering, sorting and counting to the data source instead of doing the work itself. When a formula is delegable, Power Apps translates it into a query, Dataverse evaluates it across the whole table however large it is, and only the matching rows come back. When a formula is not delegable, Power Apps has to retrieve rows and evaluate them locally, and it will only retrieve up to the data row limit. That is why delegation matters far more than it sounds like it should: it is not a performance setting, it is the difference between a question answered against your whole table and a question answered against an arbitrary first slice of it.
Should I just increase the data row limit to two thousand?
No, and it is worth being blunt about it because it is the first thing most teams try. It makes the app slower, because every non delegable operation now downloads four times as much data on whatever connection the user has. It does not fix correctness, because two thousand of five thousand rows is still a partial answer. And it hides the defect until the table grows, at which point the same bug returns with less context around it. The only defensible use is a table you can genuinely guarantee will stay small. While you are diagnosing, set the limit back to five hundred deliberately, because if the gallery contents change when you do, you have just proved which formula is not being delegated.
Which Power Fx functions are delegable to Dataverse?
The shape to remember is that simple comparisons between a column and a value delegate, and anything that computes does not. Filter, LookUp and Sort delegate with equals, not equals, greater than, less than and the And, Or and Not operators, StartsWith delegates on text columns, and the aggregates CountRows, Sum, Average, Min and Max delegate within documented limits. What does not delegate is the in operator used as a substring test, text functions such as Len, Left, Mid, Upper and Lower applied to a column inside the predicate, filtering on a column of a related record, sorting by an expression, and the table shaping functions AddColumns, GroupBy, Distinct and Ungroup. Microsoft revises this per connector, so confirm against the current delegation documentation, and trust the warning Power Apps Studio shows you as you type over any list you read on a web page including this one.
How do I write a delegable search box over a large Dataverse table?
Use StartsWith rather than the in operator, because StartsWith delegates against Dataverse text columns and the in operator used as a contains test does not. Keep the column on the left of the comparison and the search text on the right, with no functions wrapped around the column. Enable delayed output on the text input so the query runs once when typing pauses rather than once per character, and guard the filter so it does not run at all until two or three characters have been typed. If the business genuinely requires a contains search across several columns rather than a starts with search, accept that it is a server side requirement and answer it with a normalized search column maintained by a plugin or with a custom API, rather than by leaving a non delegable formula in the app and hoping the table stays small.
Is it better to use a collection instead of querying Dataverse directly?
For a large table, no, and the collection is often what made the app slow in the first place. ClearCollect over a five thousand row table is capped at the data row limit, so the collection silently contains a fraction of your data, the user waits through the whole download before the first screen appears, and the copy goes stale as soon as somebody else edits a record. A gallery pointed straight at the data source with a delegable filter loads a page of rows at a time and pages as the user scrolls, which is both faster to first paint and correct. Collections earn their place for small, stable reference data such as option lists and configuration rows, and for data the user is composing before it is saved.
Why does my gallery stutter while scrolling even though the filter is delegated?
Almost always something in the row template making its own request per row. A LookUp call to fetch a related value is the usual culprit, and two of them doubles the traffic. Image and file columns rendered in the row are another, since each one is a separate download. A delegated filter fixes the initial query but does nothing about work the template does per rendered row, which is exactly why Monitor is the right first step: a single slow query and a hundred fast ones look identical to the user and need completely different fixes. Resolve the related value from the lookup column already on the row, denormalize the field you need, or fetch the small related set once before the gallery renders.
What do I do when the query genuinely cannot be delegated?
Move it to the server rather than working around it in the app. There are four supported routes. Store the answer in a Dataverse column maintained by a plugin or a Power Automate flow, so the app filters on a plain column that delegates. Use a saved Dataverse view where the filter is fixed rather than user driven. Build a custom API or a flow that takes parameters and returns the small result set, called once rather than iterated. Or, where the list control itself is the constraint at volume, build a PCF control with server side paging. All four are more work than a formula change and all four are correct, which is the trade being made. Raising the data row limit is not on the list.
How do I prove the app is actually faster after the changes?
Take a Monitor trace before you start and another after, on the same screen, against a table with production row counts. Compare three numbers: how long the first query took, how many rows it returned, and how many separate network calls one scroll of the gallery produced. That last number is the one that usually collapses, because removing lookups from the row template and enabling explicit column selection tend to matter more than anything else on a list screen. Then run the correctness test alongside it: search for a record you know sits well past row five hundred in source order and confirm the gallery finds it. Speed and correctness are the same fix here, so verify both or you have only measured half of it.
Why does my Power Apps canvas app take 30 seconds to load, and how do I fix it?
Almost always because OnStart is doing work for every screen in the app before the first one appears: collecting whole tables, running lookups for screens the user may never open, and navigating after a chain of queries. Record a cold start in Monitor, list what OnStart does and which screen needs each item, and flag calls that return the full data row limit or repeat against the same table. Then fix in order of effort: move non essential loads to the screens that use them, replace Navigate in OnStart with App.StartScreen, run the remaining loads with Concurrent or as named formulas, cache small reference data once, flatten the first screen gallery templates, and move aggregation to the server. Measure again after each change.
Should I use named formulas or OnStart in a canvas app?
For values that are definitions rather than a sequence of actions, named formulas in App.Formulas are usually the better choice, because they are evaluated when first used rather than all at start, and they stay current without being reset by hand. OnStart still has a place for genuinely imperative start up work. On an inherited app the practical move is gradual: take the pieces of OnStart that simply define a value and turn them into named formulas one at a time, testing as you go. Named formulas are a relatively recent feature, so check the current Microsoft documentation for their status and limitations. For extracting shared logic into named formulas and components more broadly, see our guide to refactoring a tangled Power Apps solution.
How do I handle a delegation warning on a Dataverse table with hundreds of thousands of records?
Treat it as a defect that must be closed, not a performance warning. The data row limit cannot go above two thousand, so a non delegable formula over hundreds of thousands of rows is filtering a tiny, arbitrary slice and showing it as the full answer. Rewrite the formula so it delegates, make a selective filter mandatory so the screen never opens on the whole table, and let Dataverse page the results. Where the requirement is an aggregate or a search that cannot delegate, move it to the server with a maintained column, a summary table updated by a flow or plugin, or a custom API. Archive closed records out of the working table so it stays manageable.
Should a canvas app cache data in collections to load faster?
Only small, slow changing reference data, loaded once per session: option lists, configuration rows, sites, categories. That removes repeated calls without the risks of caching large tables. Large transactional tables should stay queried at the source with delegable filters, because a collection is capped at the data row limit, goes stale when other users edit records, and makes start up slower rather than faster. The common mistake is rebuilding reference collections in OnVisible on every screen visit, which turns a one time cost into a recurring one.
When is a canvas app the wrong tool for a very large dataset?
When users need to search, slice or analyse across the entire history rather than work on a bounded set of records, or when the app is effectively a high volume transactional system. Analysis across the whole table belongs in Power BI or a model driven app view, search across many columns belongs in a server side API, and some high volume processes fit a model driven app or a custom built application better than a canvas app. The signal is that every performance fix has already been applied and the screen still has to touch far more data than a person can work with. At that point the architecture conversation is cheaper than more tuning.
We recommend the right solution - whether that's Microsoft Dynamics 365, Power Platform, or a custom-built CRM. Some businesses need the Microsoft ecosystem. Others need full control without licensing. We deliver both.
Can you send us the screen that will not load?
Tell us the table, the row count, and the formula in the Items property, and we will tell you whether it is a delegation rewrite, a payload problem, a start up problem, or a screen that was never bounded. Solzet is a Dynamics 365 Customer Engagement and Power Platform consultancy in Yerevan, Armenia, working with clients and Microsoft partners across Europe and the US.