Canvas App Performance: 10 Patterns That Actually Make a Difference
Your canvas app takes 8 seconds to load. Users complain. You Google "Power Apps performance" and get a list of 47 vague tips like "reduce complexity" and "optimize your data sources." Helpful, right?
I have spent years profiling slow canvas apps, and the truth is that 80% of performance problems come from the same 10 mistakes. Fix these, and your app goes from painful to snappy. Here are the patterns that actually move the needle, with concrete before/after examples.
Why Canvas Apps Get Slow
Before the patterns, understand the three bottleneck categories:
- Data calls — Too many, too large, or non-delegable queries hitting the 500/2000 row limit
- Rendering — Too many controls on screen, complex galleries, heavy media
- Startup — Everything crammed into App.OnStart, blocking the first screen
Every pattern below targets one of these. Let us get into it.
Pattern 1: Use Concurrent() for Parallel Data Loading
The problem: App.OnStart calls five data sources sequentially. Each takes 1-2 seconds. Total: 8 seconds.
Before:
// App.OnStart — sequential, slow
ClearCollect(colEmployees, Employees);
ClearCollect(colDepartments, Departments);
ClearCollect(colProjects, Projects);
ClearCollect(colSettings, AppSettings);
ClearCollect(colRoles, SecurityRoles);
After:
// App.OnStart — parallel, fast
Concurrent(
ClearCollect(colEmployees, Employees),
ClearCollect(colDepartments, Departments),
ClearCollect(colProjects, Projects),
ClearCollect(colSettings, AppSettings),
ClearCollect(colRoles, SecurityRoles)
)
Impact: Five 1.5-second calls go from 7.5s total to ~1.5s. The calls run in parallel. This is the single highest-impact change you can make.
Pattern 2: Respect Delegation Limits
The problem: You filter a SharePoint list with 10,000 items using a non-delegable function. Power Apps silently returns only the first 500 (or 2000) rows. Your app shows incomplete data and nobody notices until a critical record is "missing."
Before:
// Non-delegable — only processes first 2000 rows locally
Filter(LargeList, StartsWith(Title, TextInput1.Text) And Status.Value = "Active")
After:
// Delegable — server processes all 10,000 rows
Filter(LargeList, StartsWith(Title, TextInput1.Text), Status.Value = "Active")
Key rules:
- Use commas instead of
And/Orin Filter — commas are delegable,Andoften is not StartsWithis delegable;inoperator andSearchfunction are not with SharePoint- Check the delegation warning (blue underline) — never ignore it
Pattern 3: Cache Lookup Data in Collections
The problem: Every gallery item calls LookUp(Departments, ID = ThisItem.DeptID).Name — that is one network call per row, per render.
Before:
// In gallery template — fires for EVERY visible row
LookUp(Departments, ID = ThisItem.DepartmentID).DepartmentName
After:
// In App.OnStart — load once
ClearCollect(colDepartments, Departments);
// In gallery template — instant local lookup
LookUp(colDepartments, ID = ThisItem.DepartmentID).DepartmentName
Impact: Goes from N network calls to 0. For a gallery showing 50 rows, this alone can save 5+ seconds of rendering time.
Pattern 4: Optimize Gallery Rendering
The problem: Your gallery has 25 controls per row: labels, icons, buttons, nested galleries. Even scrolling feels laggy.
Rules of thumb:
- Max 8-10 controls per gallery row. If you need more, rethink the layout
- Use DelayOutput on search boxes to avoid filtering on every keystroke:
// On the TextInput control
DelayOutput: true
- Set gallery LoadingSpinner to
LoadingSpinner.Noneif you handle loading states yourself - Reduce TemplatePadding and avoid unnecessary containers
Before: Gallery with 22 controls, nested gallery for tags, image control with dynamic URL.
After: Gallery with 8 controls, tags shown as concatenated text, image loaded only on detail screen.
Pattern 5: Use Named Formulas Instead of Variables
The problem: App.OnStart sets 15 global variables. Some depend on others. The order matters. It is fragile and slow.
Before:
// App.OnStart
Set(varCurrentUser, User());
Set(varUserEmail, varCurrentUser.Email);
Set(varUserDept, LookUp(Employees, Email = varUserEmail).Department);
Set(varIsManager, CountRows(Filter(Employees, Manager = varUserEmail)) > 0);
After:
// App.Formulas (named formulas — evaluated lazily)
currentUser = User();
userEmail = currentUser.Email;
userDept = LookUp(Employees, Email = userEmail).Department;
isManager = CountRows(Filter(Employees, Manager = userEmail)) > 0;
Impact: Named formulas are lazily evaluated — they only compute when referenced, and Power Apps caches the result. This moves work out of OnStart and into on-demand computation. Bonus: they automatically recalculate when dependencies change.
Pattern 6: Move Logic Out of App.OnStart
The problem: App.OnStart does everything — load data, check permissions, set themes, navigate. Users stare at a loading screen for 10 seconds.
The fix: Use App.StartScreen instead of Navigate() in OnStart:
// App.StartScreen (evaluated before OnStart completes)
If(
Param("screen") = "admin", AdminScreen,
Param("screen") = "reports", ReportsScreen,
HomeScreen
)
Then move screen-specific data loading to each screen's OnVisible event. The first screen loads in 2 seconds instead of waiting for all data.
Pattern 7: Build Component Libraries
The problem: You copy-paste a header component across 15 screens. Every change requires editing all 15. Each copy adds controls to the total count.
The fix: Create a Component Library with reusable components (headers, navigation, cards). Import the library into your app.
Benefits:
- Single source of truth for shared UI
- Reduced total control count
- Independent versioning — update the library, apps get updates automatically
Pattern 8: Optimize Media
The problem: Your app embeds 12 high-resolution images. The app package is 15MB. First load takes forever on mobile.
| Media Type | Bad Practice | Good Practice |
|---|---|---|
| Icons | Embedded PNGs (50KB each) | SVG or built-in icons (1KB) |
| Photos | Full resolution in gallery | Thumbnails, full on detail only |
| Logos | 2000x2000 PNG | 200x200 optimized PNG |
| Videos | Embedded in app | Stream URL or external link |
Rule: Keep total app media under 2MB. Use external storage (Blob, SharePoint) for anything large, and load it only when the user navigates to that screen.
Pattern 9: Reduce Control Count
The problem: Your app has 500+ controls across 20 screens. The editor is slow. The app is slow. Everything is slow.
Target: Stay under 300 total controls. Here is how:
- Replace visible/hidden control pairs with a single control using conditional properties
- Use
Switchon a single label instead of 5 overlapping labels - Delete screens you built "just in case"
- Consolidate forms — one EditForm with conditional fields beats three separate forms
Before: 5 labels toggling visibility based on status
// Label1.Visible = Status = "New"
// Label2.Visible = Status = "In Progress"
// Label3.Visible = Status = "Completed"
// Label4.Visible = Status = "Cancelled"
// Label5.Visible = Status = "On Hold"
After: 1 label with dynamic text and color
// Label1.Text
Switch(Status, "New", "New", "In Progress", "In Progress", ...)
// Label1.Color
Switch(Status, "New", Color.Blue, "In Progress", Color.Orange, ...)
Pattern 10: Use the Performance Profiler
The problem: You are guessing where the bottleneck is. Stop guessing.
Power Apps has a built-in Monitor tool (formerly Performance Profiler). Here is how to use it:
- Open your app in the Studio
- Go to Advanced tools > Monitor
- Play the app with Monitor attached
- Perform the slow action
- Check the Duration column — sort descending
What to look for:
- Network calls > 1 second: Optimize the query or cache the result
- Repeated identical calls: You are missing a collection cache
- Gallery render events > 500ms: Too many controls or heavy formulas per row
- Non-delegable warnings: Your filter is pulling all rows to the client
Pro tip: Export Monitor data to CSV for trend analysis. Run it before and after each optimization. Numbers do not lie.
The Performance Checklist
Before you ship any canvas app to production, verify:
- App.OnStart uses Concurrent() for parallel data loads
- No blue delegation warning underlines in formulas
- Lookup tables cached in collections
- Gallery rows have < 10 controls each
- Named Formulas used instead of OnStart variables where possible
- App.StartScreen handles initial navigation
- Total control count < 300
- Total media size < 2MB
- Monitor shows no calls > 2 seconds
- Tested on mobile with a real cellular connection
Key Takeaway
Canvas app performance is not mysterious. Profile first, fix the biggest bottleneck, measure again. In my experience, Concurrent() plus delegation fixes plus collection caching solve 80% of performance complaints. Do those three things before you touch anything else. Your users will notice the difference on the first launch.
Comments
No comments yet. Be the first!