Excelgoodies logo +44 (0)20 3769 3689

LEARN THIS HANDS ON

Power Apps & Power Automate

. Live Online FILLING FAST
View all upcoming batches
Power Apps Performance Tuning: Practical Patterns for Faster Galleries, Forms, and Dataverse Calls

Power Apps Performance Tuning: Practical Patterns for Faster Galleries, Forms, and Dataverse Calls

Power Apps performance tuning is mostly about a handful of repeatable patterns: how you load data, how you filter it, and how you talk to Dataverse. This article walks through concrete patterns you can apply today to speed up galleries, forms, and server calls in real apps, not just demos. If you’re building larger solutions across the Power Platform, these patterns sit nicely alongside more structured platform skills like a solid Power Apps & Power Automate workflow.


1. Core Principles: What Actually Makes Power Apps Slow

Before touching formulas, it helps to know where the bottlenecks usually are.

Typical causes:

  • Too many data calls
    • Multiple Filter() / LookUp() calls per control
    • Repeated calls in OnVisible or OnStart that could be cached
  • Delegation issues
    • Non-delegable filters on large Dataverse tables
    • Complex expressions that force client-side evaluation
  • Heavy controls and layouts
    • Galleries with many nested controls and complex OnSelect logic
    • Forms bound directly to large tables with heavy validation
  • Chatty Dataverse usage
    • Multiple Patch() calls for related records
    • Saving on every change instead of batching

Performance tuning is mainly:

  1. Reducing the number of server calls.
  2. Making remaining calls delegable.
  3. Doing less work per screen and per user action.

2. Faster Galleries: Load Smart, Filter Smart

Galleries are often the slowest part of an app. The good news: they respond well to a few simple patterns.

2.1 Pre-load data once, reuse everywhere

Instead of calling Dataverse in every gallery, load once and reuse.

Pattern: Use OnStart or OnVisible to populate collections.

Example (App OnStart):

// Load core data once
ClearCollect(colProjects, Projects);
ClearCollect(colTasks, Tasks);

// Optionally, preload reference data
ClearCollect(colStatuses, Statuses);

Then bind galleries to the collections:

Items: colProjects

Benefits:

  • Fewer round-trips to Dataverse.
  • Simple filters and sorts become instant.

When not to use this:

  • Very large tables where you only need a small subset.
  • Scenarios where real-time updates are critical (then mix direct Dataverse calls with collections).

2.2 Use delegable filters first, client filters second

For large tables, always push the heavy filtering to Dataverse.

Good pattern:

Items: Filter(Projects, Status = 'Status (Projects)'.Active)

Then refine locally:

Items:
    Filter(
        Filter(Projects, Status = 'Status (Projects)'.Active),
        StartsWith(Title, txtSearch.Text)
    )

The outer Filter is delegable (simple equality); the inner StartsWith may be delegable depending on connector, but if not, you’re at least filtering a smaller set.

2.3 Avoid non-delegable “smart” filters on big tables

Common slow pattern:

Items:
    Filter(
        Projects,
        IsBlank(Manager) || DateDiff(StartDate, Today()) < 30
    )

This can easily break delegation.

Better pattern: Split into delegable and non-delegable parts:

// Delegable part
With({
    baseProjects: Filter(Projects, DateDiff(StartDate, Today()) < 30)
},
    Filter(baseProjects, IsBlank(Manager))
)

Or pre-process into a collection:

// OnStart or OnVisible
ClearCollect(
    colRecentProjects,
    Filter(Projects, DateDiff(StartDate, Today()) < 30)
);

// Gallery Items
Filter(colRecentProjects, IsBlank(Manager))

2.4 Reduce controls inside galleries

Every control inside a gallery multiplies rendering work.

Practical rules:

  • Use 1-3 labels instead of many.
  • Avoid nested galleries unless absolutely necessary.
  • Move complex logic out of Text properties into variables or collections.

Example: heavy label calculation

Text:
    If(
        ThisItem.Status = 'Status (Projects)'.Delayed,
        "Delayed by " & DateDiff(ThisItem.DueDate, Today()) & " days",
        "On track"
    )

Better pattern: precompute once when data loads.

// OnStart or OnVisible
ClearCollect(
    colProjects,
    AddColumns(
        Projects,
        "StatusText",
        If(
            Status = 'Status (Projects)'.Delayed,
            "Delayed by " & DateDiff(DueDate, Today()) & " days",
            "On track"
        )
    )
);

// Label in gallery
Text: ThisItem.StatusText

3. Faster Forms: Decouple, Cache, and Validate Efficiently

Forms are convenient, but the defaults are not always fast.

3.1 Use forms only for submit/view, not for all logic

Slow pattern:

  • Complex logic in Update properties of card controls.
  • Many cards bound directly to Dataverse columns with heavy formulas.

Better pattern:

  • Use a form for binding and submit.
  • Keep complex calculations in variables or collections.

Example: set a variable before editing a form.

// OnSelect of gallery row
Set(varSelectedProject, ThisItem);
EditForm(frmProject);

Bind the form to the variable:

Item: varSelectedProject

This reduces repeated LookUp() calls and keeps the form lean.

3.2 Limit fields per form and use tabs

Single huge forms slow down rendering.

Patterns:

  • Split into multiple forms (e.g., General, Financial, Status).
  • Use a tab menu to show/hide forms:
// Tab selection
Set(varActiveTab, "General");

// Visible properties
frmGeneral.Visible: varActiveTab = "General"
frmFinancial.Visible: varActiveTab = "Financial"

3.3 Validate on submit, not on every keystroke

Real-time validation feels nice but can hammer performance.

Slow pattern:

  • Heavy If() or LookUp() in OnChange of text inputs.

Better pattern:

  • Lightweight checks on change.
  • Full validation in OnSuccess or OnSelect of the save button.

Example: simple flag on change

// OnChange of txtBudget
UpdateContext({
    ctxBudgetInvalid: Value(txtBudget.Text) < 0
});

Full validation on save:

// OnSelect of btnSave
If(
    ctxBudgetInvalid,
    Notify("Budget must be positive", NotificationType.Error),
    SubmitForm(frmProject)
);

4. Dataverse Performance: Batch, Minimise, and Shape Calls

Dataverse is powerful, but chatty usage kills performance. Focus on the shape and frequency of calls.

4.1 Batch Patch() operations with collections

Slow pattern:

// Inside a ForAll over a large set
ForAll(colTasksToUpdate,
    Patch(Tasks, ThisRecord, { Status: 'Status (Tasks)'.Completed })
);

This fires a call per record.

Better pattern: use Patch() with a collection (where possible) or reduce the set first.

// Filter down first
ClearCollect(
    colTasksToComplete,
    Filter(colTasks, ShouldComplete = true)
);

// Then patch in one go (connector-dependent)
Patch(Tasks, colTasksToComplete, { Status: 'Status (Tasks)'.Completed });

If the connector doesn’t support collection patching, at least ensure colTasksToComplete is small and pre-filtered.

4.2 Avoid repeated LookUp() to Dataverse

Repeated LookUp() calls are a common hidden cost.

Slow pattern:

// Label in gallery
Text:
    LookUp(Users, UserId = ThisItem.OwnerId).FullName

This runs once per row.

Better pattern: join once using AddColumns.

// OnStart or OnVisible
ClearCollect(colUsers, Users);
ClearCollect(
    colProjectsWithOwner,
    AddColumns(
        Projects,
        "OwnerName",
        LookUp(colUsers, UserId = OwnerId).FullName
    )
);

// Gallery
Items: colProjectsWithOwner

// Label
Text: ThisItem.OwnerName

Now the LookUp runs once per project instead of per render.

4.3 Use Concurrent() for independent calls

When you need multiple datasets, load them in parallel.

// OnStart
Concurrent(
    ClearCollect(colProjects, Projects),
    ClearCollect(colTasks, Tasks),
    ClearCollect(colUsers, Users)
);

This cuts overall load time when the calls don’t depend on each other.

4.4 Cache reference data locally

Reference tables (statuses, categories, small lookups) are perfect for caching.

Pattern:

// OnStart
ClearCollect(colStatuses, Statuses);
ClearCollect(colCategories, Categories);

// Controls
Items: colStatuses
Items: colCategories

You avoid hitting Dataverse for every dropdown, and the app feels much snappier.


5. Screen-Level Patterns: When and Where to Load

Performance is also about when you ask for data.

5.1 Use OnStart for global data, OnVisible for screen-specific

Guideline:

  • App OnStart:
    • User profile
    • Reference tables
    • Core master data used across multiple screens
  • Screen OnVisible:
    • Data only needed for that screen
    • Context-specific lists (e.g., tasks for selected project)

Example:

// App OnStart
Set(varCurrentUser, User());
ClearCollect(colStatuses, Statuses);
ClearCollect(colProjects, Projects);

// Screen OnVisible
ClearCollect(
    colTasksForProject,
    Filter(Tasks, ProjectId = varSelectedProject.ProjectId)
);

5.2 Avoid heavy logic in OnVisible when navigating frequently

If users bounce between screens, heavy OnVisible code will be felt.

Pattern:

  • Move expensive logic to once-only triggers (e.g., OnStart, initial load).
  • On OnVisible, only refresh what genuinely changes.

Example: instead of reloading all projects each time:

// Bad: Screen OnVisible
ClearCollect(colProjects, Projects);

// Better: App OnStart
ClearCollect(colProjects, Projects);

// Screen OnVisible: only refresh if needed
If(varProjectsDirty,
    ClearCollect(colProjects, Projects);
    Set(varProjectsDirty, false)
);

Set varProjectsDirty to true after any operation that changes projects.


6. Measuring and Iterating: Don’t Guess, Observe

You don’t get a built-in profiler, but you can still measure.

Practical tactics:

  • Stopwatch variables
    • Capture timestamps before and after heavy operations.

Example:

// Before data load
Set(varLoadStart, Now());
ClearCollect(colProjects, Projects);
Set(varLoadDuration, DateDiff(varLoadStart, Now(), TimeUnit.Seconds));

// Show on debug label
Text: "Load: " & varLoadDuration & "s"
  • Temporary debug labels

    • Display counts: CountRows(colProjects).
    • Display delegation warnings in the formula bar and adjust.
  • Gradual rollout

    • Apply patterns to the worst-performing screen first.
    • Compare perceived load time before and after.

7. One Concrete Takeaway: Standardise Your Load Pattern

Pick one screen with a slow gallery, and refactor it to this standard pattern:

  1. Load data once into a collection (OnStart or OnVisible).
  2. Use delegable filters first, local refinements second.
  3. Precompute heavy label text via AddColumns.

Once you see the difference, apply the same pattern to the rest of your app – it’s the fastest way to get real performance gains without rewriting everything.

Power Apps

New

Next Batches Now Live

Power BIPower BI
SQLSQL
Power AppsPower Apps
Power AutomatePower Automate
Microsoft FabricMicrosoft Fabrics
AzureAzure Data Engineering
Explore Dates & Reserve Your Spot → Reserve Your Spot →