Wicked Smart Data
LearnArticlesAbout
Sign InSign Up
LearnArticlesAboutContact
Sign InSign Up
Wicked Smart Data

The go-to platform for professionals who want to master data, automation, and AI — from Excel fundamentals to cutting-edge machine learning.

Platform

  • Learning Paths
  • Articles
  • About
  • Contact

Connect

  • Contact Us
  • RSS Feed

© 2026 Wicked Smart Data. All rights reserved.

Privacy PolicyTerms of Service
All Articles
Canvas App Performance Profiling in Production: Monitor, Telemetry & Azure Application Insights

Canvas App Performance Profiling in Production: Monitor, Telemetry & Azure Application Insights

Power Apps🔥 Expert29 min readJul 20, 2026Updated Jul 20, 2026
Table of Contents
  • Introduction
  • Prerequisites
  • Understanding the Canvas App Execution Model First
  • Using Power Apps Monitor: Beyond the Basics
  • Opening Monitor for a Production Session
  • Reading the Monitor Event Stream
  • What Monitor Reveals That You Can't See in the Editor
  • Downloading and Diffing Monitor Sessions
  • Instrumenting Your App with `Trace()` for Custom Telemetry
  • Designing a Telemetry Schema
  • Measuring Screen Load Duration
  • Wrapping Data Operations in Telemetry

Canvas App Performance Profiling in Production: Identifying and Resolving Bottlenecks with Monitor, Telemetry, and Azure Application Insights Integration

Introduction

Your Canvas app passed UAT. Users loved the demo. Then it went to production with 400 concurrent users pulling from a SharePoint list with 80,000 rows, and now the ops team is getting Slack messages every morning about the "slow app." Screens take 8 seconds to load. The gallery flickers. Someone submits a form and nothing happens for 12 seconds. You open the app in the editor, click through it yourself, and everything feels fine. You're now staring at the gap between development-time intuition and production-scale reality — and that gap is exactly where most Canvas app performance work lives.

Performance profiling in Canvas apps is uniquely tricky because the platform hides a lot. Unlike a Node.js API where you can attach a profiler and watch the call stack, Power Apps abstracts away the execution model. Most developers end up guessing — removing formulas one by one, switching data sources, adding loading spinners — without ever confirming what the actual bottleneck is. This article is about replacing guessing with evidence. By the end, you'll be able to systematically capture, analyze, and act on real performance data from your Canvas apps using Power Apps Monitor, custom telemetry patterns, and Azure Application Insights.

What you'll learn:

  • How to use the Power Apps Monitor tool to capture and interpret network traces, formula evaluation timelines, and delegation warnings during live production sessions
  • How to instrument your Canvas apps with custom telemetry using Trace() and structured logging patterns
  • How to connect Canvas apps to Azure Application Insights for persistent, queryable production telemetry
  • How to read Kusto Query Language (KQL) queries against your telemetry data to identify specific bottlenecks by screen, user, formula, and data source
  • How to triage and resolve the five most common Canvas app performance anti-patterns, with before/after architectural comparisons

Prerequisites

This lesson assumes you're already comfortable building Canvas apps with complex formulas, data sources, and multi-screen navigation. You should have:

  • A Power Apps per-user or per-app license (or equivalent in your tenant)
  • Maker access to at least one Canvas app in a production or staging environment
  • An Azure subscription with permission to create Application Insights resources
  • Familiarity with basic KQL syntax (helpful but not required — we'll explain the queries we write)
  • A working understanding of Power Apps delegation and its limitations

Understanding the Canvas App Execution Model First

Before you can profile anything effectively, you need a mental model of what is actually executing. Canvas apps are not web apps in the traditional sense — there's no DOM manipulation, no JavaScript runtime you control, and no server-side rendering pipeline to inspect. What you actually have is a declarative formula engine running inside the Power Apps player runtime, which is itself a web application hosted at make.powerapps.com or inside Teams.

When a user opens your app, the player runtime fetches the app definition (a binary-compiled representation of your app's formulas and assets), initializes the screen graph, and begins evaluating formulas in dependency order. Every property of every control — Visible, Items, Default, Color, X, Y — is a formula. The engine tracks dependencies between formulas and re-evaluates downstream formulas when upstream values change. This is the reactive model that makes Canvas apps feel alive, and it's also the source of most performance problems.

Three distinct phases contribute to perceived performance:

Load time — The time from a user clicking "open app" to the first interactive screen being visible. This includes fetching the app package, loading assets, evaluating App.OnStart, and rendering the initial screen.

Navigation time — The time between a user tapping a navigation button and the target screen becoming interactive. This includes evaluating the new screen's OnVisible formulas and hydrating its controls.

Interaction time — The time between a user action (button press, gallery selection, text input) and a visible response. This includes formula evaluation, data source calls, and re-render.

Most "slow app" complaints map to one of these three phases. Your profiling strategy needs to capture data at each phase to identify which one is actually causing pain.


Using Power Apps Monitor: Beyond the Basics

Power Apps Monitor is the official debugging and profiling tool, accessible from the maker portal at make.powerapps.com. Most developers know it exists and have maybe looked at the network tab. Few use it to its full potential.

Opening Monitor for a Production Session

Here's the thing most documentation misses: Monitor can attach to published apps running in production, not just apps open in the editor. This matters enormously because the editor environment has fundamentally different performance characteristics — it injects the editing runtime, the formula bar, the tree view, and dozens of other components that change how the app behaves.

To attach Monitor to a production session:

  1. Open the maker portal and navigate to your app
  2. Select the app and click the ellipsis menu, then choose "Monitor" (not "Edit")
  3. In the Monitor window that opens, you'll see a "Link" icon at the top — this generates a special session URL
  4. Copy that URL and send it to a real user (or open it yourself in an incognito window as a non-maker)
  5. When that user opens the link and runs the app, their session appears in your Monitor window

You are now watching a real production execution in real time. Everything that executes — every formula evaluation, every network call, every error — appears in the Monitor log as the user interacts with the app.

Reading the Monitor Event Stream

The Monitor event stream is organized into event types. The ones that matter most for performance work are:

Network — HTTP requests made by the app. Each row shows the URL, method, status code, duration (in milliseconds), and the request/response body. This is where you'll find slow SharePoint queries, slow Dataverse reads, and throttled connector calls.

Formula — Evaluations of significant formulas. Not every formula evaluation appears here (trivial calculations are silent), but data-source-touching formulas and complex functions like Filter(), LookUp(), Sort(), and Collect() do.

UserAction — Button presses, navigation events, gallery selections. These serve as anchors in the timeline that let you correlate user behavior with subsequent formula and network events.

Error — Formula errors, delegation warnings that became runtime failures, connector errors.

Publish — Metadata about the app version running in the session.

When you're investigating a slow screen load, find the UserAction event for the navigation (or the Publish event for the initial load), then look at every Network and Formula event that follows until the screen is interactive. The duration of each event is shown in the "Duration (ms)" column. Sort by duration descending and you immediately see your most expensive operations.

What Monitor Reveals That You Can't See in the Editor

The single most valuable thing Monitor shows you is N+1 query patterns. This is a problem you cannot detect by looking at formulas — you have to see the actual HTTP calls being made.

Consider a gallery bound to a SharePoint list of 200 work orders. Each gallery item has a label that shows the name of the assigned technician, pulled from a separate "Employees" list using a formula like:

LookUp(Employees, ID = ThisItem.AssignedTechnicianID, FullName)

In the editor, with 10 items visible, this works fine. In Monitor on a production session with the gallery fully loaded, you'll see 200 separate HTTP requests to the Employees list — one for each item. Each request takes 200-400ms. The gallery is serializing 200 network round trips. That's where your 12-second load time is going.

This is the N+1 problem, and you would never know it was happening without watching the network trace. The fix is to pre-load the Employees collection on screen load and use a local LookUp() against the in-memory collection instead. We'll cover this architectural pattern in detail later.

Tip: In Monitor, use the "Filter" option to show only events from a specific event category. When you're hunting for network performance issues, filter to "Network" only and sort by "Duration (ms)" descending. The top five rows almost always contain your bottleneck.

Downloading and Diffing Monitor Sessions

One underused feature: you can export a Monitor session as a JSON file using the download button in the toolbar. This lets you:

  • Compare a fast session against a slow session for the same user flow
  • Share a session with a colleague who wasn't watching live
  • Build a baseline and compare it after you make performance changes

When you export two sessions and compare the "Duration (ms)" values for the same event sequences, you can quantitatively confirm whether a change you made actually improved performance. This is how you avoid the trap of making a change, feeling like the app "seems faster," and calling it done.


Instrumenting Your App with `Trace()` for Custom Telemetry

Monitor is excellent for interactive debugging sessions, but it has two significant limitations for production work at scale: it requires a live watcher, and the data doesn't persist anywhere by default. For persistent, queryable telemetry, you need to instrument your app with Trace().

Trace() is a Canvas app function that emits a custom event to the Monitor log (during a monitored session) and, critically, to Azure Application Insights if you've connected it. The signature is:

Trace(message, severity, customRecord)

Where:

  • message is a string identifier for the event
  • severity is one of TraceSeverity.Information, TraceSeverity.Warning, TraceSeverity.Error, or TraceSeverity.Critical
  • customRecord is an optional record (a Power Apps record literal) of custom properties you want to attach

The third parameter is where the real power lives. You can attach arbitrary structured data to each trace event, and that data becomes queryable dimensions in Application Insights.

Designing a Telemetry Schema

Before you write a single Trace() call, design your schema. Ad-hoc telemetry is almost as useless as no telemetry — if every developer adds Trace() calls with different field names and formats, your queries become unmaintainable.

Here's a schema that works well for Canvas app performance telemetry:

// Naming convention for message: "ScreenName_EventType_Description"
// Standard custom properties:
{
  screen: "WorkOrderList",          // Which screen triggered the event
  user: User().Email,               // Who triggered it (be mindful of privacy)
  sessionId: varSessionId,          // A GUID generated at App.OnStart for correlation
  appVersion: Param("appVersion"),  // Passed as a query parameter for version tracking
  durationMs: varLoadDuration,      // Measured duration in milliseconds
  recordCount: CountRows(colData),  // Row counts for data operations
  dataSource: "SharePoint_WorkOrders", // Identifies which data source was called
  success: true,                    // Outcome flag
  errorMessage: ""                  // Error detail if applicable
}

To generate a session ID at app start, put this in App.OnStart:

Set(varSessionId, Text(GUID()))

This gives you a correlation ID that lets you query Application Insights for all events from a single user's session — which is invaluable when you're reconstructing a slow user journey.

Measuring Screen Load Duration

To measure how long a screen actually takes to become usable, you need to capture a timestamp when the screen starts loading and another when it's done. Here's the pattern:

In the screen's OnVisible property (the first thing that runs when the screen is navigated to):

// Record when loading started
Set(varScreenLoadStart, Now());

// Perform your data loads
ClearCollect(
    colWorkOrders,
    Filter(
        WorkOrders,
        Status = "Open" And AssignedRegion = varUserRegion
    )
);

// Calculate duration and emit telemetry
Set(varScreenLoadDuration, DateDiff(varScreenLoadStart, Now(), TimeUnit.Milliseconds));

Trace(
    "WorkOrderList_Screen_Load",
    TraceSeverity.Information,
    {
        screen: "WorkOrderList",
        user: User().Email,
        sessionId: varSessionId,
        durationMs: varScreenLoadDuration,
        recordCount: CountRows(colWorkOrders),
        dataSource: "SharePoint_WorkOrders",
        success: true,
        errorMessage: ""
    }
)

Warning: Now() in Canvas apps does not give you sub-second precision in all runtime environments. For coarse-grained timing (seconds to tens of seconds), it works fine. For millisecond-level timing, use a combination of Trace() timestamps in Application Insights, which are timestamped with high precision at the time the event is received.

Wrapping Data Operations in Telemetry

For individual data operations, create a consistent pattern. Here's a reusable approach for a Patch() call that creates a work order:

// In a button's OnSelect:
Set(varOperationStart, Now());

// Attempt the operation
Set(varNewWorkOrder,
    Patch(
        WorkOrders,
        Defaults(WorkOrders),
        {
            Title: txtTitle.Text,
            Priority: ddlPriority.Selected.Value,
            AssignedTechnicianID: galTechnicians.Selected.ID,
            Status: "Open",
            CreatedBy: User().Email
        }
    )
);

// Check outcome and emit telemetry
If(
    IsError(varNewWorkOrder),
    Trace(
        "WorkOrderDetail_Patch_Create",
        TraceSeverity.Error,
        {
            screen: "WorkOrderDetail",
            user: User().Email,
            sessionId: varSessionId,
            durationMs: DateDiff(varOperationStart, Now(), TimeUnit.Milliseconds),
            dataSource: "SharePoint_WorkOrders",
            success: false,
            errorMessage: "Patch returned error - check connector permissions"
        }
    ),
    Trace(
        "WorkOrderDetail_Patch_Create",
        TraceSeverity.Information,
        {
            screen: "WorkOrderDetail",
            user: User().Email,
            sessionId: varSessionId,
            durationMs: DateDiff(varOperationStart, Now(), TimeUnit.Milliseconds),
            dataSource: "SharePoint_WorkOrders",
            success: true,
            errorMessage: ""
        }
    )
)

This pattern gives you both timing and outcome data for every significant operation. Over time in Application Insights, you can compute P50/P90/P99 latencies for each operation and identify which users or time windows show elevated latency.


Connecting Canvas Apps to Azure Application Insights

This is where telemetry goes from "useful during a debug session" to "operational intelligence you can query at 2am when someone calls about a production incident."

Creating the Application Insights Resource

In the Azure portal, create an Application Insights resource:

  1. Search for "Application Insights" in the search bar and open the service
  2. Click "Create" and choose your subscription, resource group, and region
  3. Give it a name like wsd-canvasapps-prod and choose "Workspace-based" as the resource mode (the classic mode is being deprecated)
  4. After creation, open the resource and navigate to "Overview" — you'll see the Instrumentation Key and Connection String at the top. Copy the Connection String.

Tip: Use a single Application Insights instance for all your Canvas apps in a given environment (development, staging, production). This makes cross-app queries possible and reduces Azure billing complexity. Use custom properties like appName in your telemetry schema to filter between apps.

Connecting the App to Application Insights

In the Power Apps maker portal, with your app open in the editor:

  1. Click the wrench icon (Settings) in the left sidebar, or go to "Settings" from the top menu
  2. Navigate to "Upcoming features" and ensure you're on the "Experimental" tab
  3. Scroll to find "Azure Application Insights" and enable the toggle
  4. A new field appears asking for your "Instrumentation Key" — paste the instrumentation key (not the full connection string) from your Azure resource here
  5. Save and republish the app

Once this is configured, every Trace() call in your app will send events to Application Insights as custom traces events. Additionally, Power Apps itself will automatically send some diagnostic telemetry about session starts, errors, and performance metrics — though this automatic telemetry is less structured than what you emit with Trace().

Warning: As of the current platform state, the Azure Application Insights integration in Canvas apps uses the instrumentation key, not the newer connection string. The instrumentation key is still functional, but be aware that Microsoft has announced that connection string-based configuration is preferred for new workloads. Monitor the Power Apps release notes for updates to this integration.

Verifying Events Are Arriving

After publishing your updated app, open it, navigate through a few screens, perform some operations, and then return to your Application Insights resource in Azure. Navigate to "Logs" (the query interface), and run this basic KQL query:

traces
| where timestamp > ago(1h)
| order by timestamp desc
| take 50

You should see rows with your trace messages in the message column. Expand any row and click "customDimensions" — you should see all the properties you passed in the third argument to Trace().

If nothing shows up after 5 minutes, the most common causes are: the wrong instrumentation key, the app wasn't republished after adding the key, or the experimental feature wasn't saved. Recheck each step.


Querying Your Telemetry with KQL

Now comes the part that separates performance professionals from people who have telemetry but can't use it. KQL (Kusto Query Language) is the query language for Azure Monitor and Application Insights, and it's genuinely pleasant to write once you understand its pipe-based structure.

Every KQL query starts with a table name, then applies a series of transformations separated by | (pipe) operators. Each pipe operator filters, transforms, or aggregates the data. Let's build the queries you'll actually use.

Query 1: Screen Load Time Distribution

This query shows you the P50, P90, and P99 load times for each screen, sorted by P99 descending. Start with P99 — that's where your worst-affected users live.

traces
| where timestamp > ago(7d)
| where message endswith "_Screen_Load"
| extend
    screenName = tostring(customDimensions.screen),
    durationMs = todouble(customDimensions.durationMs),
    userEmail = tostring(customDimensions.user)
| where isnotempty(durationMs)
| summarize
    P50 = percentile(durationMs, 50),
    P90 = percentile(durationMs, 90),
    P99 = percentile(durationMs, 99),
    Count = count()
    by screenName
| order by P99 desc

If your WorkOrderList_Screen_Load shows P50=2100ms, P90=6800ms, P99=14200ms, you immediately know this screen has a long tail — most users have an acceptable experience, but a meaningful percentage are hitting something much worse. That asymmetric distribution is a classic sign of a delegation problem or a cold-start issue on a connector.

Query 2: Identifying Slow Users vs. Slow Operations

Sometimes slowness is user-specific (network conditions, device performance) and sometimes it's operation-specific (a query that returns too much data). This query helps you separate the two:

traces
| where timestamp > ago(7d)
| extend
    screenName = tostring(customDimensions.screen),
    durationMs = todouble(customDimensions.durationMs),
    userEmail = tostring(customDimensions.user),
    dataSource = tostring(customDimensions.dataSource)
| where isnotempty(durationMs) and isnotempty(screenName)
| summarize
    AvgDuration = avg(durationMs),
    MaxDuration = max(durationMs),
    Samples = count()
    by userEmail, screenName
| order by AvgDuration desc
| take 20

If the top rows all show the same user, investigate that user's device and network. If the top rows show many different users all hitting the same screen, the problem is in the operation, not the user.

Query 3: Error Rate Over Time

A performance investigation often reveals errors disguised as slowness — timeouts that eventually return an error after 30 seconds. This query shows you your error rate trend:

traces
| where timestamp > ago(14d)
| extend
    isError = customDimensions.success == "false" or severityLevel == 3
| summarize
    TotalEvents = count(),
    ErrorEvents = countif(tostring(customDimensions.success) == "false")
    by bin(timestamp, 1h)
| extend ErrorRate = round(100.0 * ErrorEvents / TotalEvents, 2)
| order by timestamp asc
| project timestamp, TotalEvents, ErrorEvents, ErrorRate

Render this as a time chart in the Application Insights query editor to see if error rates spike at particular times — end of business day, Monday mornings after the weekend batch job runs, etc.

Query 4: Session Reconstruction

When a user reports that "the app was broken for me this morning," you can use their email and your session ID to reconstruct exactly what happened:

traces
| where timestamp > ago(24h)
| where tostring(customDimensions.user) == "jane.doe@contoso.com"
| extend
    sessionId = tostring(customDimensions.sessionId),
    screenName = tostring(customDimensions.screen),
    durationMs = todouble(customDimensions.durationMs),
    success = tostring(customDimensions.success)
| order by timestamp asc
| project timestamp, message, screenName, durationMs, success, sessionId

This gives you a chronological log of every telemetry event Jane triggered, grouped by session. You can see which screen she was on, what happened, how long each operation took, and whether anything failed.

Query 5: Data Volume Correlation

Your telemetry schema includes recordCount. This query tells you whether load time correlates with how many records were returned — which would indicate a delegation or local processing problem:

traces
| where timestamp > ago(7d)
| where message == "WorkOrderList_Screen_Load"
| extend
    durationMs = todouble(customDimensions.durationMs),
    recordCount = toint(customDimensions.recordCount)
| where isnotempty(durationMs) and isnotempty(recordCount)
| summarize avg(durationMs) by
    RecordBucket = case(
        recordCount < 100, "0-99",
        recordCount < 500, "100-499",
        recordCount < 1000, "500-999",
        "1000+"
    )
| order by RecordBucket asc

If you see avg duration jump from 1200ms for the "0-99" bucket to 8500ms for the "1000+" bucket, you have a local processing problem — the app is pulling all records from the server and filtering/sorting them in the client. That's a delegation issue, and no amount of formula optimization will fix it. You need to push the filtering to the data source.


The Five Performance Anti-Patterns and How to Resolve Them

Armed with evidence from Monitor and Application Insights, let's look at the five problems you'll encounter most frequently, why they happen, and how to fix them structurally.

Anti-Pattern 1: App.OnStart Overload

The most common cause of slow initial load is an App.OnStart that does too much. Every ClearCollect(), Collect(), and Set() call in App.OnStart executes sequentially before any screen renders. Developers put things here because "it only runs once" — but for the user, "once" means "before they can do anything."

What Monitor shows: A long string of sequential Network events immediately after app launch, with nothing user-visible happening. Total duration might be 15-30 seconds.

The fix — Lazy loading: Move data loads out of App.OnStart and into the OnVisible property of the screen that actually needs them. Only load what the first screen needs before it renders. Load everything else after the user is already interacting.

For data you genuinely need app-wide (like a user's role or preferences), keep it in App.OnStart but minimize it:

// App.OnStart - only essential global state
Concurrent(
    Set(varCurrentUser, {
        email: User().Email,
        displayName: User().FullName,
        role: LookUp(UserRoles, Email = User().Email, Role)
    }),
    Set(varAppConfig, LookUp(AppConfiguration, IsActive = true))
)

Note the Concurrent() wrapper. This executes both Set() calls in parallel rather than sequentially. For two calls that don't depend on each other, this can halve your OnStart time.

Anti-Pattern 2: Delegation Violations at Scale

Delegation is Power Apps' way of telling the server "you do the filtering/sorting, don't send me everything." When a formula is not delegable, the server sends the first 500 (or 2000, if you've increased the limit) records, and then Power Apps filters that local collection in memory. If your actual dataset has 50,000 records, you're working with an invisible, silent truncation of your data.

What Monitor shows: A single large network response (sometimes 1-2MB of JSON for 2000 records) followed by correct-looking results in the UI, but incorrect counts — CountRows() returns 500 when you know there are more records.

What Application Insights shows: Your recordCount metric plateaus at exactly 500 or 2000 across all users, regardless of how much data they should see.

The fix: Rewrite non-delegable formulas. For SharePoint and Dataverse, the set of delegable functions is well-documented. The non-delegable ones you'll hit most often:

  • CountRows() on a filtered datasource (not delegable to SharePoint)
  • Filter() with In operator against a collection (not delegable)
  • Search() across multiple columns (partially delegable)
  • String functions like Len(), Left(), Mid() inside Filter() (not delegable)

For Search() across multiple columns in SharePoint, the delegable alternative is to use SharePoint's native search via a custom connector, or to pre-process and index a single combined search field in SharePoint itself.

For complex filtering that can't be made delegable, consider using Dataverse as your data source instead of SharePoint. Dataverse's delegation support is significantly more comprehensive.

Anti-Pattern 3: Gallery N+1 Queries

We introduced this earlier. It deserves a full structural treatment.

What Monitor shows: Dozens to hundreds of sequential network requests, all to the same endpoint, all retrieving single records. Each request takes 150-400ms. A gallery with 100 items generates 100 lookups.

The fix — Pre-load and join locally:

// In Screen.OnVisible:
// 1. Load the primary collection
ClearCollect(colWorkOrders, Filter(WorkOrders, Status = "Open"));

// 2. Load the related collection (the one being looked up per item)
ClearCollect(
    colTechnicians,
    Filter(Employees, IsActive = true)
);

// 3. Create a joined collection using AddColumns
ClearCollect(
    colWorkOrdersWithTechnicians,
    AddColumns(
        colWorkOrders,
        "TechnicianName",
        LookUp(colTechnicians, ID = TechnicianID, FullName),
        "TechnicianPhone",
        LookUp(colTechnicians, ID = TechnicianID, Phone)
    )
);

Now bind the gallery to colWorkOrdersWithTechnicians. The LookUp() inside AddColumns() operates against colTechnicians, which is an in-memory collection — no network calls. Two network requests total instead of 102.

Warning: This pattern moves data loading time from "gallery scroll time" to "screen load time." Your screen will now take longer to show the first pixel, but it will be fully interactive when it appears. For large collections (>500 rows), communicate this loading state clearly with a LoadingSpinner or progress indicator.

Anti-Pattern 4: Form Re-Rendering Storms

Complex screens with many formulas can trigger re-render cascades — a single variable change causes dozens of formula re-evaluations, each of which may trigger further evaluations. The user experiences this as the screen "locking up" briefly after any interaction.

What Monitor shows: Long chains of Formula events with short individual durations but high aggregate duration. Nothing is slow in isolation, but everything adds up.

The diagnosis: Look for variables that are referenced by many controls. If varSelectedWorkOrder is referenced in 40 places on a screen, changing it triggers 40 formula re-evaluations. If those formulas in turn change other variables, you get cascading re-evaluations.

The fix — Consolidate into records:

Instead of 15 individual variables for the selected item's display state:

// Anti-pattern: many individual variables
Set(varSelectedTitle, galWorkOrders.Selected.Title);
Set(varSelectedStatus, galWorkOrders.Selected.Status);
Set(varSelectedPriority, galWorkOrders.Selected.Priority);
Set(varSelectedTechnician, galWorkOrders.Selected.TechnicianName);
// ...12 more of these

Use a single record variable:

// Better: one variable change, one dependency update
Set(varSelectedWorkOrder, galWorkOrders.Selected);

Then reference fields directly: varSelectedWorkOrder.Title, varSelectedWorkOrder.Status, etc. The dependency graph now has one variable instead of fifteen, reducing re-evaluation cascades significantly.

Anti-Pattern 5: Synchronous Multi-Step Operations

Buttons that perform several sequential operations — validate input, check business rules, write to SharePoint, send an email notification, write to an audit log — create a perceptible freeze while the user waits for each step to complete before the next begins.

What Monitor shows: 4-6 sequential network events after a button press, each waiting for the previous to complete. Total duration: 8-15 seconds.

The fix — Use Concurrent() for independent operations and Power Automate for post-write work:

For truly independent operations:

// Write to both lists simultaneously instead of sequentially
Concurrent(
    Patch(WorkOrders, varDraftWorkOrder, {Status: "Submitted"}),
    Patch(AuditLog, Defaults(AuditLog), {
        Action: "WorkOrderSubmitted",
        UserEmail: User().Email,
        Timestamp: Now()
    })
)

For post-write operations like sending emails, don't do them in the Canvas app at all. Use a Power Automate flow triggered by a SharePoint item change. The user's button press writes the record and returns immediately — the email sends asynchronously in the background via Power Automate. From the user's perspective, the operation is instant.


Hands-On Exercise

In this exercise, you'll instrument an existing Canvas app with telemetry, connect it to Application Insights, and use a Monitor session to identify a performance problem.

Setup: You'll need a Canvas app connected to a SharePoint list. If you don't have one ready, create a simple app with a gallery showing items from any SharePoint list with at least 200 rows.

Step 1: Create an Application Insights resource in Azure. Navigate to the Azure portal, create a new Application Insights resource in a resource group of your choice, and copy the instrumentation key from the Overview page.

Step 2: Connect the app to Application Insights. Open your Canvas app in the editor. Go to Settings, find the Azure Application Insights toggle under Experimental features, enable it, and paste your instrumentation key. Save the settings.

Step 3: Add a session ID variable. In App.OnStart, add Set(varSessionId, Text(GUID())). This is your correlation ID.

Step 4: Instrument your main screen. On your gallery screen, add the following to the beginning of Screen.OnVisible:

Set(varScreenLoadStart, Now())

And at the end of Screen.OnVisible (after your data loads), add:

Set(varScreenLoadMs, DateDiff(varScreenLoadStart, Now(), TimeUnit.Milliseconds));
Trace(
    "GalleryScreen_Load",
    TraceSeverity.Information,
    {
        screen: "GalleryScreen",
        user: User().Email,
        sessionId: varSessionId,
        durationMs: varScreenLoadMs,
        recordCount: CountRows(colYourCollection),
        success: true
    }
)

Step 5: Publish and open Monitor. Publish the app. Open Monitor from the maker portal. Generate a session link and open it in a separate incognito window. Navigate through the app, triggering your gallery screen at least five times.

Step 6: Analyze the Monitor network trace. In the Monitor window, filter to "Network" events only. Sort by Duration descending. Note the top three longest-running requests and write down: the URL, the duration, and whether you can identify which formula triggered it.

Step 7: Query Application Insights. Wait 5-10 minutes for telemetry to propagate. Open Application Insights Logs and run the screen load duration query from earlier in this article. Verify your events appear. Add a filter for tostring(customDimensions.sessionId) == "your-session-id" to see only your test session's events.

Step 8 (Challenge): In Monitor, look for any N+1 query patterns — multiple sequential requests to the same SharePoint list URL. If you find one, modify your app to pre-load the related collection and measure the improvement by running another Monitor session and comparing durations.


Common Mistakes & Troubleshooting

"My Trace() calls work in Monitor but nothing shows up in Application Insights." The most likely cause is the instrumentation key not being saved, or the app not being republished after setting the key. Verify in App Settings that the key is present. Republish, wait 10 minutes, and query Application Insights again. Also verify you're querying the right Application Insights resource — it's easy to have multiple and query the wrong one.

"My telemetry shows durationMs of 0 for all events." This happens when Now() calls are evaluated at the same logical time step. Canvas apps batch formula evaluations within a single OnSelect or OnVisible call, and two sequential Now() calls within the same logical batch may return identical values. Workaround: use the timestamp of events in Application Insights for duration measurement instead of computing duration inside the app.

"Monitor shows fast response times but users say the app is slow." This is often a rendering issue, not a data issue. If the data loads quickly but the screen has hundreds of controls or deeply nested galleries, rendering time adds latency that Monitor doesn't capture as network time. Consider reducing the number of visible controls on slow screens and using Visible = false (not DisplayMode = Disabled) to remove controls from the render tree when they're not needed.

"My Concurrent() call seems to run sequentially in some cases." Concurrent() has undocumented limitations. It doesn't parallelize calls to the same data source — if you call ClearCollect(List1, ...) and ClearCollect(List2, ...) where both connect to the same SharePoint site, they may serialize anyway due to connection pooling. Verify parallelism using Monitor by checking if the network event start times overlap.

"Application Insights costs are higher than expected." Every Trace() call sends data to Azure, and you're billed on data ingestion. If your app has many active users and you've instrumented every button press, you may generate significant telemetry volume. Use sampling by only calling Trace() for operations above a duration threshold, or for a random sample of users using Mod(Value(Right(varSessionId, 4)), 10) = 0 (traces 10% of sessions). Review the Azure Cost Management console for your Application Insights resource's ingestion volume.

"I can see network events in Monitor but can't tell which formula triggered them." Enable verbose logging in Monitor by clicking the settings icon (gear) in the Monitor toolbar and enabling "Include Power Fx formula events." This increases the verbosity of Formula events and often shows the formula that preceded a network call. The correlation isn't always perfect, but it narrows the search significantly.

"My P99 latency is much higher than P90, but I can't find a pattern." High P99 with normal P90 is often caused by cold starts — the first user to hit a connector after a period of inactivity triggers a cold start penalty (sometimes 5-30 seconds for custom connectors). Query Application Insights for events where durationMs > P90_value and check the timestamp values — if they cluster at Monday morning, after midnight, or after holidays, you have a cold start pattern. Solutions include warm-up flows that ping the connector on a schedule, or switching to a connector with faster cold start behavior.


Summary & Next Steps

Performance profiling in Canvas apps requires combining three distinct tools for a complete picture: Power Apps Monitor for real-time, interactive debugging and network-level visibility; Trace() instrumentation for capturing performance context at the formula level; and Azure Application Insights for persistent, queryable telemetry at production scale.

The workflow you've built here — define a telemetry schema, instrument your app, query Application Insights with KQL, and correlate findings with Monitor network traces — gives you the same kind of evidence-based performance engineering that backend engineers take for granted. You're no longer guessing. You're measuring.

The five anti-patterns we covered — OnStart overload, delegation violations, gallery N+1 queries, re-render storms, and synchronous multi-step operations — cover the vast majority of Canvas app performance problems you'll encounter in practice. Each has a structural solution, and each solution is verifiable through your telemetry.

What to explore next:

  • Power Apps Capacity Planning — Learn how to use Application Insights telemetry to build capacity forecasts and justify infrastructure changes before performance degrades
  • Custom Connectors with Caching — Explore patterns for building custom connectors that return cached responses for common queries, reducing data source call frequency
  • Dataverse as a High-Performance Backend — If SharePoint limitations are repeatedly hitting you, study the Dataverse delegation model and the performance implications of different table types (standard vs. elastic tables)
  • Environment-Level Performance Monitoring with Power Platform Admin Center — The Admin Center provides tenant-wide app performance analytics that complement your app-specific Application Insights telemetry
  • Power Automate Integration Patterns — Study async processing patterns where Canvas apps offload heavy operations to Power Automate flows, communicating completion via status polling or push notifications through Dataverse

The investment in telemetry and profiling infrastructure pays forward with every new feature you add. When the next performance complaint arrives, you'll have a query ready before the second Slack message lands.

Learning Path: Canvas Apps 101

Previous

Canvas App Error Handling: Building Resilient Apps with IfError, Notify, and Graceful Failure Patterns*

Related Articles

Power Apps⚡ Practitioner

Canvas App Error Handling: Building Resilient Apps with IfError, Notify, and Graceful Failure Patterns*

20 min
Power Apps🌱 Foundation

Power Apps Data Validation: Using If, IsBlank, and IsMatch to Prevent Bad Data in Forms

15 min
Power Apps🔥 Expert

Canvas App State Management at Scale: Global Variables, Named Formulas, and Context Isolation for Enterprise Apps

28 min

On this page

  • Introduction
  • Prerequisites
  • Understanding the Canvas App Execution Model First
  • Using Power Apps Monitor: Beyond the Basics
  • Opening Monitor for a Production Session
  • Reading the Monitor Event Stream
  • What Monitor Reveals That You Can't See in the Editor
  • Downloading and Diffing Monitor Sessions
  • Instrumenting Your App with `Trace()` for Custom Telemetry
  • Designing a Telemetry Schema
  • Connecting Canvas Apps to Azure Application Insights
  • Creating the Application Insights Resource
  • Connecting the App to Application Insights
  • Verifying Events Are Arriving
  • Querying Your Telemetry with KQL
  • Query 1: Screen Load Time Distribution
  • Query 2: Identifying Slow Users vs. Slow Operations
  • Query 3: Error Rate Over Time
  • Query 4: Session Reconstruction
  • Query 5: Data Volume Correlation
  • The Five Performance Anti-Patterns and How to Resolve Them
  • Anti-Pattern 1: App.OnStart Overload
  • Anti-Pattern 2: Delegation Violations at Scale
  • Anti-Pattern 3: Gallery N+1 Queries
  • Anti-Pattern 4: Form Re-Rendering Storms
  • Anti-Pattern 5: Synchronous Multi-Step Operations
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps
  • Measuring Screen Load Duration
  • Wrapping Data Operations in Telemetry
  • Connecting Canvas Apps to Azure Application Insights
  • Creating the Application Insights Resource
  • Connecting the App to Application Insights
  • Verifying Events Are Arriving
  • Querying Your Telemetry with KQL
  • Query 1: Screen Load Time Distribution
  • Query 2: Identifying Slow Users vs. Slow Operations
  • Query 3: Error Rate Over Time
  • Query 4: Session Reconstruction
  • Query 5: Data Volume Correlation
  • The Five Performance Anti-Patterns and How to Resolve Them
  • Anti-Pattern 1: App.OnStart Overload
  • Anti-Pattern 2: Delegation Violations at Scale
  • Anti-Pattern 3: Gallery N+1 Queries
  • Anti-Pattern 4: Form Re-Rendering Storms
  • Anti-Pattern 5: Synchronous Multi-Step Operations
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps