PowerApps Performance Optimization: Delegation, Collections, and Best Practices
Introduction
PowerApps performance directly impacts user adoption. Slow apps frustrate users and reduce productivity. This guide covers delegation to handle large datasets, collection optimization, caching strategies, concurrent operations, component performance, and monitoring tools to build responsive canvas apps that scale.
Understanding Delegation
What is Delegation?
Delegation means PowerApps sends query logic to the data source (SharePoint, Dataverse, SQL) rather than downloading all records to the device. Non-delegable queries download the first 500 (or 2000 max) records, then filter/sort locallyβmissing data beyond that limit.
Delegable operations:
| Data Source | Delegable Functions |
|---|---|
| Dataverse | Filter, Search, Sort, LookUp, Sum, Average, Min, Max, CountRows |
| SharePoint | Filter (limited), Search, Sort, LookUp |
| SQL Server | Filter, Search, Sort, LookUp, Sum, Average, Min, Max, CountRows |
| Excel | None (all non-delegable) |
Delegation warnings (yellow triangles):
// β οΈ Non-delegable - Downloads first 2000, filters locally
Filter(LargeSharePointList, Status = "Active" && DateCreated > Today() - 30)
// β
Delegable - Filters on server
Filter(DataverseTable, Status = "Active" And DateCreated > Today() - 30)
Delegation Limits
App settings:
File β Settings β Advanced settings
βββ Data row limit for non-delegable queries: 500 (default) to 2000 (max)
Warning: Increasing limit doesn't solve delegation issuesβdownloads more data, making app slower.
Fixing Delegation Issues
Example: Filter by text contains (non-delegable in SharePoint):
// β Non-delegable - only checks first 500/2000 records
Filter(SharePointList, "keyword" in Title)
// β
Solution 1: Use Search (delegable)
Search(SharePointList, "keyword", "Title")
// β
Solution 2: Add indexed column and exact match
Filter(SharePointList, Status = "Active") // Status is indexed
Multiple conditions (AND/OR):
// β Non-delegable - complex formula
Filter(Items, Status = "Active" && (Priority = "High" || Priority = "Critical"))
// β
Delegable - simplified
Filter(Items, Status = "Active" And Priority = "High")
// Run second query for Critical, then combine with Concat or Union
Calculated columns (always non-delegable):
// β Non-delegable - Year() function
Filter(Orders, Year(OrderDate) = 2025)
// β
Solution: Add date range filter
Filter(Orders, OrderDate >= Date(2025,1,1) And OrderDate <= Date(2025,12,31))
Collection Optimization
When to Use Collections
Good use cases:
- Cache static data (products, categories, configuration)
- Temporary storage for multi-step forms
- Performance optimization for repeated queries
- Offline data storage
Bad use cases:
- Large datasets (>5000 records) - Use delegation instead
- Frequently changing data - Query directly
- Complex filtering - Let data source handle it
Efficient Collection Loading
Load on app start (OnStart):
// β Slow - Three sequential calls (6+ seconds)
ClearCollect(colProducts, Products);
ClearCollect(colCategories, Categories);
ClearCollect(colUsers, Users)
// β
Fast - Concurrent calls (2 seconds)
Concurrent(
ClearCollect(colProducts, Products),
ClearCollect(colCategories, Categories),
ClearCollect(colUsers, Users)
)
// β
Better - Load only what's needed
Concurrent(
ClearCollect(colProducts,
Filter(Products, Status = "Active")
),
ClearCollect(colCategories, Categories),
Set(varCurrentUser, User())
)
Collection vs. Variables
Collection (table):
// Multi-row data
ClearCollect(colSelectedItems,
{ID: 1, Name: "Item A"},
{ID: 2, Name: "Item B"}
)
// Use in gallery
Gallery1.Items = colSelectedItems
Variable (single value or record):
// Single value
Set(varTotalAmount, 1500)
// Single record
Set(varCurrentItem, LookUp(Items, ID = 123))
// Use in label
Label1.Text = varCurrentItem.Title
Global vs. Context variables:
// Global variable (accessible everywhere)
Set(gblUserRole, "Manager")
// Context variable (screen-specific)
UpdateContext({locScreenLoading: true})
// Best practice: Use context variables for screen state
UpdateContext({
locSelectedCategory: Blank(),
locFilterText: "",
locShowDetails: false
})
Caching Strategies
Static Data Caching
Cache lookup data on start:
// App.OnStart
Concurrent(
ClearCollect(colStatusChoices,
Choices('Cases'.'Status')
),
ClearCollect(colPriorityChoices,
Choices('Cases'.'Priority')
),
ClearCollect(colDepartments,
Departments // Small reference table
)
);
Set(varAppReady, true)
// Use cached data in dropdown
Dropdown_Status.Items = colStatusChoices
Lazy Loading Pattern
Load data only when screen is accessed:
// Screen1.OnVisible
If(IsEmpty(colProducts),
ClearCollect(colProducts,
Filter(Products, Status = "Active")
)
)
// Check if cache is stale (refresh after 5 minutes)
If(DateDiff(varLastProductsLoad, Now(), Minutes) > 5,
ClearCollect(colProducts, Products);
Set(varLastProductsLoad, Now())
)
Refresh Strategy
Manual refresh button:
// RefreshButton.OnSelect
UpdateContext({locRefreshing: true});
Concurrent(
ClearCollect(colProducts, Products),
ClearCollect(colOrders,
Filter(Orders, Status = "Pending")
)
);
UpdateContext({locRefreshing: false})
Auto-refresh with timer:
// Timer control
Duration: 300000 // 5 minutes in milliseconds
AutoStart: true
Repeat: true
OnTimerEnd:
Refresh(Products);
Refresh(Orders)
Gallery Performance
Pagination Pattern
Load 50 items at a time:
// Global variables on screen load
Set(varPageSize, 50);
Set(varCurrentPage, 1);
UpdateContext({locTotalPages: RoundUp(CountRows(Products) / varPageSize, 0)})
// Gallery items
Gallery_Products.Items =
FirstN(
Filter(Products, Status = "Active"),
varPageSize * varCurrentPage
)
// Next button
Button_Next.OnSelect =
If(varCurrentPage < locTotalPages,
Set(varCurrentPage, varCurrentPage + 1)
)
// Previous button
Button_Prev.OnSelect =
If(varCurrentPage > 1,
Set(varCurrentPage, varCurrentPage - 1)
)
// Page counter label
Label_PageInfo.Text =
"Page " & varCurrentPage & " of " & locTotalPages
Gallery Item Optimization
Minimize controls per item:
// β Slow - 10 controls per item Γ 100 items = 1000 controls
Gallery with:
βββ 3 Labels
βββ 2 Icons
βββ 3 Buttons
βββ 1 Image
βββ 1 Checkbox
// β
Fast - 4 controls per item Γ 100 items = 400 controls
Gallery with:
βββ 1 HTML text (combines 3 labels)
βββ 1 Icon (conditional color)
βββ 1 Button with icon
βββ 1 Image
HTML text for multiple labels:
// Instead of 3 separate labels:
"<div style='padding:10px'>
<h3>" & ThisItem.Title & "</h3>
<p>" & ThisItem.Description & "</p>
<small>Created: " & Text(ThisItem.Created, "mm/dd/yyyy") & "</small>
</div>"
Visible Property Optimization
Use gallery visibility, not item visibility:
// β Slow - Evaluates for every item
Gallery_Products.Items = Products
Item1_Container.Visible = ThisItem.Status = "Active"
// β
Fast - Filter data source
Gallery_Products.Items = Filter(Products, Status = "Active")
Item1_Container.Visible = true
Formula Optimization
Avoid Nested Lookups
// β Slow - Lookup for every gallery item
Gallery_Orders.Items = Orders
Label_CustomerName.Text =
LookUp(Customers, ID = ThisItem.CustomerID).Name
// β
Fast - Expand relationship in data source
Gallery_Orders.Items = Orders
Label_CustomerName.Text = ThisItem.Customer.Name
// β
Alternative - Join collections on app start
ClearCollect(colOrdersWithCustomers,
AddColumns(Orders,
"CustomerName",
LookUp(Customers, ID = Orders[@CustomerID]).Name
)
)
With() Function for Repeated Expressions
// β Slow - Calculates LookUp 3 times
If(
LookUp(Products, ID = varSelectedID).Price > 1000,
Set(varDiscount, LookUp(Products, ID = varSelectedID).Price * 0.1),
Set(varDiscount, LookUp(Products, ID = varSelectedID).Price * 0.05)
)
// β
Fast - Calculates LookUp once
With(
{selectedProduct: LookUp(Products, ID = varSelectedID)},
If(
selectedProduct.Price > 1000,
Set(varDiscount, selectedProduct.Price * 0.1),
Set(varDiscount, selectedProduct.Price * 0.05)
)
)
ForAll for Bulk Operations
// β Slow - Loop executes sequentially
ForAll(colSelectedItems,
Patch(Orders, Defaults(Orders), {
ProductID: ID,
Quantity: 1,
OrderDate: Today()
})
)
// β
Fast - Single Patch call
Patch(Orders,
ForAll(colSelectedItems,
{
ProductID: ID,
Quantity: 1,
OrderDate: Today()
}
)
)
Component Best Practices
Input Properties vs. Internal State
// Component: ProductCard
// β Slow - Re-renders on every parent change
ProductCard.ProductID = Gallery1.Selected.ID
// Inside component: product data fetched on every change
// β
Fast - Pass complete record
ProductCard.Product = Gallery1.Selected
// Inside component: use Product.ID directly
Component Output Properties
// Inside ProductCard component
// Define custom output property
OnSelect = Set(locSelectedProduct, Product); UpdateContext({locShowDetails: true})
// Component output property definition
Name: SelectedProduct
Type: Record
Formula: locSelectedProduct
// Parent screen uses output
If(!IsBlank(ProductCard1.SelectedProduct),
Navigate(DetailScreen, ScreenTransition.Fade, {
itemID: ProductCard1.SelectedProduct.ID
})
)
Reusable Form Components
// Component: DynamicForm
// Input properties:
βββ FormSchema (Table of field definitions)
βββ FormData (Record)
βββ OnSubmitAction (Behavior)
// Usage in parent screen
DynamicForm1.FormSchema = [
{FieldName: "Title", Type: "Text", Required: true},
{FieldName: "Priority", Type: "Dropdown", Choices: colPriorityChoices},
{FieldName: "DueDate", Type: "Date", Required: true}
]
DynamicForm1.FormData = Gallery1.Selected
DynamicForm1.OnSubmitAction =
Patch(Cases, Gallery1.Selected, DynamicForm1.FormValues)
Monitoring and Debugging
Monitor Tool
Enable Monitor:
Advanced Tools β Monitor
Key metrics to track:
- Duration: Formula execution time (target <100ms)
- Data calls: Number of API requests (minimize)
- Delegation warnings: Yellow triangles indicate issues
- Error messages: Red indicators show failures
Filter for slow operations:
Monitor β Filter by duration > 500ms
App Insights Integration
Enable telemetry:
// App.OnStart
Set(varSessionID, GUID());
Set(varAppVersion, "1.2.0")
// Track custom events
Trace(
"Order Created",
TraceSeverity.Information,
{
OrderID: varNewOrderID,
CustomerID: varCustomerID,
Amount: varOrderTotal,
SessionID: varSessionID
}
)
// Track errors
IfError(
Patch(Orders, Defaults(Orders), formData),
Trace(
"Order Creation Failed",
TraceSeverity.Error,
{
ErrorMessage: FirstError.Message,
SessionID: varSessionID
}
)
)
Performance Checklist
β Data operations:
- Use delegation wherever possible
- Cache static data in collections
- Load data concurrently with Concurrent()
- Implement pagination for large datasets
β Formulas:
- Avoid nested LookUps in galleries
- Use With() for repeated expressions
- Use ForAll for bulk operations
- Minimize calculations in gallery items
β Controls:
- Limit controls per gallery item (<5)
- Use HTML text instead of multiple labels
- Hide screens with Visible, not complex logic
- Optimize component input properties
β Images:
- Use SVG for icons (smaller, scalable)
- Compress images (<100KB each)
- Load images lazily (when needed)
- Cache images in collections if reused
Best Practices
- Test with Large Data: Always test with 10,000+ records
- Use Monitor Tool: Profile every screen before release
- Implement Caching: Load reference data once on start
- Lazy Load Screens: Only load data when screen is visible
- Minimize API Calls: Batch operations, use concurrent calls
- Optimize Galleries: Pagination, reduce controls per item
- Document Delegation: Comment non-delegable workarounds
Troubleshooting
App loads slowly:
- Check App.OnStart for sequential operations (use Concurrent)
- Remove unnecessary data loading
- Defer heavy operations to screen load
Gallery scrolling is laggy:
- Reduce controls per gallery item
- Remove complex formulas in item templates
- Implement pagination instead of loading all items
Forms submit slowly:
- Batch Patch operations
- Remove duplicate data calls
- Check for nested LookUps
Key Takeaways
- Delegation is critical for apps with >500 records
- Collections cache data but shouldn't replace delegation
- Concurrent() reduces app start time by 60-80%
- Gallery optimization (pagination, fewer controls) improves UX
- Monitor tool reveals performance bottlenecks
- Test with production-scale data (10,000+ records)
Next Steps
- Implement connection pooling with Dataverse
- Use virtual galleries for massive datasets (preview feature)
- Enable App Insights for production monitoring
- Explore Power Fx experimental features (query functions)
Additional Resources
Fast apps, happy users.