analyze-canvas-performance

Analyze and audit a Power Apps canvas app. USE WHEN the user wants to analyze, profile, audit, review, diagnose, or improve a Canvas App or pa.yaml files. USE…

npx skills add https://github.com/microsoft/power-cat-skills --skill analyze-canvas-performance

Analyze Canvas App Performance & Quality

Perform a full audit of the existing Canvas App and produce a prioritized findings report covering performance, coding standards, app design, and error handling:

$ARGUMENTS

CRITICAL: Review Guidance First

Before analyzing anything, you MUST read and internalize the technical reference document:

  • ${CLAUDE_PLUGIN_ROOT}/references/TechnicalGuide.md — Technical best practices, control selection, validation workflow, formulas, layout strategies

Read this file before planning any analysis.

CRITICAL: Sync the Canvas App First

Before reading any YAML files, call the sync_canvas MCP tool to ensure a local copy of the canvas app YAML is present and up to date.

Only proceed after sync_canvas completes successfully.

CRITICAL: Never Modify YAML Files

You MUST read .pa.yaml files for analysis purposes only. Never write, edit, create, or delete any .yaml or .pa.yaml file at any point during this skill execution. The YAML files are the source of truth for the app and must remain unchanged.


Analysis Workflow

0. Ask for Reviewer Name

Before doing anything else, ask the user:

What name should appear as the author of this report? (used in "Audited by", "Reviewed by", and "Generated by" fields)

Store the answer as {{AUTHOR}}. If the user provides no answer, use "Power Apps Team".


1. Discover App Structure

Run the following in parallel immediately after sync_canvas:

  1. Read every .pa.yaml file and build a structural inventory.
    • Important: App.pa.yaml is the app-level object file, not a screen. Never treat it as a screen or create a screen dependency entry for it. It is the source of App.OnStart, App.Formulas, App.OnError, and global app settings. Attribute findings from it to App (not a screen name).
  2. Call get_appchecker_errors — retrieves all formula errors, performance warnings, and data-source issues detected by the Power Apps App Checker engine.
  3. Call get_accessibility_errors — retrieves all accessibility issues detected by the Power Apps Accessibility Checker.

Store the raw results from both tools — they will be processed in Section 6.

From the YAML files inventory:

  • Number of screens and total control count per screen
  • All data sources (SharePoint, Dataverse, SQL, Collections)
  • All connectors and Power Automate flow calls
  • App.OnStart, App.Formulas, and App.OnError content
  • All global variables (Set()), context variables (UpdateContext()), and collections (ClearCollect()/Collect())

Share a discovery summary:

Discovery complete.

ScreenControl CountData Sources Referenced
[Name][N][list]

Total controls: [N] | Total screens: [N] | Data sources: [list] | Flow calls: [N] App Checker issues: [N] | Accessibility issues: [N]


2. Audit — Performance

For every issue found, record: Severity (🔴 High / 🟡 Medium / 🟢 Low), Location, Finding, and Recommendation.

2.1 App.OnStart Overload & Named Formulas

  • Flag heavy ClearCollect / Collect / Set chains in App.OnStart that delay startup.
  • Flag Navigate() calls inside App.OnStart — replace with App.StartScreen (declarative, doesn't block rendering):
    // GOOD
    App.StartScreen = If(Param("AdminMode") = "1", AdminScreen, HomeScreen)
    
  • Check whether global Set() variables are read-only after initialization — if so, migrate to Named Formulas (App.Formulas): they are lazy, auto-updated, and don't execute at startup.
  • Flag variables initialized in OnStart that are only used on one screen — move them to that screen's OnVisible.

2.2 Screen OnVisible & Navigation

  • Flag screens that reload data on every OnVisible when the data doesn't change between visits — add If(IsEmpty(collection), ClearCollect(...)) guards.
  • Identify screens with more than 50 controls — recommend splitting or extracting components.

2.3 Delegation & Data Queries

  • Flag every First(Filter(...)) or First(Search(...)) — replace with LookUp(...) (delegable, returns one record without loading the dataset).
  • Flag non-delegable functions in filter predicates (Left, Mid, Len, IsBlank, in operator, Search()) applied to large external tables — recommend delegable alternatives or server-side views.
  • Flag hardcoded delegation row limits (default 500) where actual record count may exceed them.

2.4 Large Data Payloads & Explicit Column Selection (ECS)

  • Flag ClearCollect(col, DataSource) calls that retrieve all columns when only a subset is needed — recommend ShowColumns() + Filter():
    // GOOD
    ClearCollect(colAcc,
        ShowColumns(Filter(Accounts, !IsBlank('Address 1: City')), "name", "address1_city")
    )
    
  • Check whether Explicit Column Selection (ECS) is enabled in app settings — flag if it's off (especially for apps created before ECS was default).
  • Flag Count(Filter(...)) patterns — replace with CountIf(DataSource, condition) for a single delegable call.

2.5 Concurrent Loading

  • Flag sequential independent ClearCollect / Collect calls in OnStart or OnVisible that each target an external data source (Dataverse, SharePoint, SQL, a connector, or a Power Automate flow) — these are HTTP calls that can run in parallel. Wrap them in Concurrent(...):
    // GOOD — each targets a different external source
    Concurrent(
        ClearCollect(colAccounts, Accounts),
        ClearCollect(colUsers, Users),
        ClearCollect(colSettings, 'Environment Variable Definitions')
    );
    
  • ⚠️ Do NOT flag calls that source from a local collection (e.g., ClearCollect(colFiltered, Filter(colLocal, ...))) — these are in-memory operations with no HTTP overhead; Concurrent() adds no benefit there.
  • ⚠️ Only apply Concurrent() to independent calls — never when one call depends on another's output.

2.6 N+1 Database Calls in Galleries

  • Flag LookUp(...), Filter(...), connector calls, or Power Automate flow calls inside gallery item properties (Label.Text, Image.Image, Icon.Color, etc.) only when the data source is external — i.e., a Dataverse table, a SharePoint list, a SQL table, an OData feed, or a named flow. Each gallery row triggers a separate HTTP call (N+1 pattern).
    // BAD — LookUp against an external Dataverse table; 1 HTTP call per row
    Label.Text = LookUp(Users, UserID = ThisItem.CreatedBy).FullName
    // GOOD — traverse the Dataverse relationship; resolved server-side
    Label.Text = ThisItem.'Created By'.'Full Name'
    
  • Do NOT flag LookUp(colLocalCollection, ...) or Filter(colLocalCollection, ...) — local collections live in memory and have zero HTTP cost.
  • Recommend: Dataverse relationships for related-record fields; AddColumns to pre-join external data into a local collection before binding the gallery; Dataverse views or SQL views to flatten data server-side.
  • Flag CountRows(Filter(ExternalSource, ...)) patterns inside gallery item formulas — triggers a full table scan per row render; replace with CountIf(ExternalSource, condition).

2.7 Nested API Calls in Formulas

  • Flag connector/API calls (e.g., Office365Users.MyProfile(), MSN.GetCurrentWeather()) nested inside Filter(), LookUp(), or ForAll() — the call fires once per evaluation:
    // BAD — API called every time the filter evaluates
    Filter(Product, Email = Office365Users.MyProfile().Mail)
    // GOOD — call once, store result
    Set(varUserEmail, Office365Users.MyProfile().Mail);
    Filter(Product, Email = varUserEmail)
    
  • Recommend caching the result in a variable at App.OnStart or via a Named Formula.

2.8 Unoptimized ForAll & Patch Usage

  • Flag nested ForAll — exponential iterations; recommend flattening data first.
  • Flag Patch() inside ForAll() — one server call per record. Invert the pattern for a single batched call:
    // BAD — N calls
    ForAll(colData, Patch(DataSource, { field: "value" }));
    // GOOD — 1 batched call
    Patch(DataSource, ForAll(colData, { field: "value" }));
    
  • Flag complex or large ForAll chains (multiple nested ForAll, AddColumns(ForAll(...)), or ForAll with multi-step Patch/Collect logic) where the data source is Dataverse — the business logic is running client-side over HTTP. Recommend moving the logic server-side:
    • Dataverse View: create a filtered/joined view in Dataverse and bind the gallery directly to it — eliminates the ForAll/AddColumns entirely.
    • Low-code Dataverse Plugin (Power Fx plugin): encapsulate the transformation logic in a server-side plugin triggered on record events.
    • Dataverse Plug-in (C#): for complex multi-table joins or aggregations that cannot be expressed as a view.
    • Custom API + Dataverse Plug-in: expose a single custom API action that accepts parameters and returns the shaped result — the app makes one HTTP call and receives fully processed business data, removing all client-side ForAll loops.
    // INSTEAD OF complex client-side ForAll:
    // Call a Custom API that returns pre-joined data
    Set(varResult, MyCustomAPI.GetOrderSummary({ customerId: gblUserId }))
    

2.9 SharePoint Lists — Recommend Migration to Dataverse

  • Identify every data source in the app that is a SharePoint list connector call.
  • Flag each one with a 🟡 Medium finding and recommend migrating to a Dataverse table for the following reasons:
    • Performance: Dataverse supports full server-side delegation for complex queries; SharePoint delegation support is limited (e.g., no Search() delegation, limited Filter operators).
    • Security: Dataverse provides row-level security (column-level security, field-level security, business unit hierarchy) that SharePoint cannot match.
    • Relational integrity: Dataverse supports typed relationships, lookups, and choices natively — SharePoint uses flat lists.
    • Scalability: Dataverse tables handle millions of rows with indexed queries; SharePoint lists degrade beyond ~5,000 items.
  • In addition, flag these SharePoint-specific performance pitfalls as separate 🟡 Medium findings:
    • Too many dynamic lookup columns: Person, Group, and Calculated columns add processing time inside SharePoint before data reaches the client — recommend replacing with static text/email columns where possible.
    • Picture or Attachment columns: inflate response payload even when not displayed — recommend removing them from the list if images are stored externally.
    • Very large lists (100k+ items): all columns are returned to the client even if the app only uses a few — ensure ECS is on and use ShowColumns() + delegable Filter() to limit returned data.
    • Partition large lists by category, year, or time window and let the user select a segment rather than querying the full list at once.
  • In the recommendation, note that a migration requires: creating the Dataverse table, migrating existing data (Power Automate or dataflow), updating all formulas to use the new table name and column schema.

2.10 Environment Variables — Batch Retrieval

  • Identify apps that retrieve values from Environment Variables (i.e., 'Environment Variable Definitions' or LookUp('Environment Variable Values', ...) calls).
  • Flag sequential individual lookups of multiple environment variables — each is a separate HTTP call to Dataverse:
    // BAD — 3 separate HTTP calls
    Set(varApiUrl, LookUp('Environment Variable Values', 'Environment Variable Definition'.schemaname = "cr123_ApiUrl").Value);
    Set(varTenantId, LookUp('Environment Variable Values', 'Environment Variable Definition'.schemaname = "cr123_TenantId").Value);
    Set(varFeatureFlag, LookUp('Environment Variable Values', 'Environment Variable Definition'.schemaname = "cr123_FeatureFlag").Value);
    
  • Recommend one of these patterns:
    1. Single ClearCollect — retrieve all needed environment variable values in one query and look up from the local collection:
      // GOOD — 1 HTTP call, then local lookups
      ClearCollect(colEnvVars, 'Environment Variable Values');
      Set(varApiUrl, LookUp(colEnvVars, ...).Value);
      
    2. Concurrent() — if the variables must each be in their own global variable, wrap all the LookUp / Set calls in Concurrent():
      // GOOD — all 3 HTTP calls fire in parallel
      Concurrent(
          Set(varApiUrl, LookUp('Environment Variable Values', ...).Value),
          Set(varTenantId, LookUp('Environment Variable Values', ...).Value),
          Set(varFeatureFlag, LookUp('Environment Variable Values', ...).Value)
      );
      

2.11 Power Automate Calls to Populate Collections

  • Flag Power Automate flows called solely to retrieve data that could be queried directly from Power Apps — each flow call adds ~600ms instantiation overhead plus network latency.
  • Recommend replacing with a direct connector call unless the flow performs server-side logic unavailable in Power Fx.

2.12 Excessive Collections & Variables

  • Flag multiple Set() calls that fetch different properties from the same API in separate calls — consolidate into one call and access properties:
    // BAD — 4 API calls
    Set(varName, Office365Users.MyProfile().DisplayName);
    Set(varCity, Office365Users.MyProfile().City);
    // GOOD — 1 API call
    Set(varEmployee, Office365Users.MyProfile())
    
  • Flag unused variables or collections (declared but never read elsewhere in the app).
  • Flag Clear(); Collect(...) patterns — replace with the single ClearCollect(...) call.

2.13 Text Input & DelayOutput

  • Flag TextInput controls used as live search/filter boxes where DelayOutput is false (default) — every keystroke retriggers gallery filtering or data calls.
  • Recommend setting DelayOutput = true to introduce a ~500ms debounce before the formula evaluates.

2.14 Cross-Screen Control References

  • Flag any formula on Screen A that references a control on Screen B (e.g., Screen2.Label3.Text) — Power Apps loads Screen B into memory prematurely, increasing initial load time.
  • Recommend replacing with shared variables, collections, or navigation context parameters.

2.15 Control Count & Visual Complexity

  • Flag screens with more than 50 controls — recommend components or container consolidation.
  • Flag Visible = false controls that are always rendered (non-component) — loading an invisible control still consumes memory; use conditional component rendering instead.
  • Identify repeated control groups across screens that could become reusable components.

2.16 Images & Media

  • Flag Image controls bound to base64 strings stored in variables or collections — recommend storing URLs instead.
  • Flag large PNG/JPG files embedded in the app package when SVG would suffice for icons and illustrations.
  • Flag unused media files and screens bundled in the app package — recommend deleting them and republishing.

2.17 Excel Connector Limitations

  • Flag any data source connected via the Excel connector (OneDrive/SharePoint-hosted .xlsx file).
  • Flag as 🟡 Medium with the following context:
    • Excel is not a relational database — it handles changes like a user editing a file; high-write scenarios (>50 updates/min) degrade performance.
    • The connector is limited to 2,000 rows regardless of table size — data beyond this limit is silently truncated.
    • Wide tables (many columns) compound latency even when ECS is enabled.
  • Recommend:
    1. Use the Excel Business Online connector if multi-user access is required.
    2. Include only the columns the app actually needs in the Excel table.
    3. Partition data into multiple smaller tables if the row limit is a constraint.
    4. Migrate to Dataverse or Azure SQL for any scenario requiring >2,000 rows or high write frequency.

2.18 Missing App Preload Setting

  • Check whether the Preload app for enhanced performance setting is enabled. This is detectable if the setting can be observed; otherwise, always flag as 🟢 Low advisory.
  • When preloading is off, compiled JavaScript bundles, media, and control assets are not downloaded until authentication completes — users on slow networks see a long blank screen.
  • Recommend enabling it:
    1. In Power Apps → select the app → Settings on the command bar.
    2. Set "Preload app for enhanced performance" to Yes.
    3. For Teams-embedded apps: remove and re-add the app to the Teams tab after enabling.
  • ⚠️ Note: preloading exposes compiled app assets (JS, images, app name, environment URL) via unauthenticated endpoints. Data from connectors still requires authentication. Flag this trade-off if the app embeds sensitive media directly in control properties.

2.19 Data Source Bottlenecks

  • When the app uses SQL Server (on-premises or Azure SQL) or Dataverse, flag patterns suggesting server-side bottlenecks as 🔴 High:
    • Formulas using the in operator on a SQL/Dataverse column instead of StartsWithin causes a table scan, StartsWith leverages an index.
    • Filter(DataSource, condition) on columns that are likely unindexed (non-PK, non-FK, non-choice columns in Dataverse; text columns without a SQL index).
    • Large Sort / SortByColumns calls on unindexed columns.
  • Recommend:
    • Add SQL indexes on columns used in Filter, LookUp, and Sort.
    • Replace in with StartsWith where prefix matching is acceptable.
    • Check Azure SQL tier (DTU/vCore) — keep CPU utilization ≤ 75% under load.
    • Check on-premises data gateway health if latency is high — consider scaling out the gateway or moving to cloud-hosted data.

2.20 Client, Browser & Geographic Factors

  • Flag as 🟢 Low advisory (not fixable from YAML, but important context for slow-app reports):
    • If the app connects to an on-premises data gateway, note that gateway latency is added to every data call — gateway should be co-located with the data source and the majority of users.
    • Different mobile platforms (iOS vs Android) have different concurrent HTTP request limits — large data sets load unevenly; recommend smaller payloads.
    • If users are geographically distributed, recommend using Azure SQL or Dataverse (cloud-hosted, globally distributed) rather than on-premises data sources.
    • Recommend always testing on the actual target device and network — not just in the browser on a developer machine.

3. Audit — Coding Standards

3.1 Naming Conventions

  • Flag controls still using default names (Button1, Label3, TextInput5) — apply the standard 3-character type prefix + camelCase:

    ControlPrefixControlPrefix
    ButtonbtnLabellbl
    TextInputtxtGallerygal
    FormfrmImageimg
    ContainerconIconico
    DropdowndrpComboBoxcmb
  • Flag global variables without a gbl prefix and context variables without a loc prefix.

  • Flag collections without a col prefix.

  • Flag screens without a plain-language name + "Screen" suffix (e.g., Home Screen, Search Screen).

3.2 Variable Scope Misuse

  • Flag global variables (Set(...)) used for state that only matters on one screen — replace with context variables (UpdateContext(...)).
  • Flag context variables used for values that must persist across screens — replace with global variables.
  • Flag variables that share the same name as both a context variable and a global variable (silent conflict).
  • Recommend Named Formulas for any read-only computed values.

3.3 Missing Code Comments

  • Flag formulas that are complex (nested If/Switch, multi-step ForAll, custom error handling) but have no comments — 🟢 Low.
  • Flag App.OnStart with more than 10 Set()/Collect() calls and no section marker comments.
  • Note: Power Apps strips all comments from the compiled package — comments have zero impact on performance, package size, or load time.
  • Recommend using both comment styles:
    // Line comment — explain the next line
    /* Block comment:
       Use for multi-line explanations or section markers */
    

3.4 Poor Code Formatting

  • Flag formulas that appear as a single long line with deeply nested functions and no line breaks — 🟢 Low.
  • Recommend using the Format Text command in the Power Apps Studio formula bar to auto-apply indentation and line breaks.
  • Consistent formatting reduces review time and makes delegation warnings easier to spot.

4. Audit — App Design & Layout

4.1 Nested Galleries

  • Flag any gallery control whose Items source contains another gallery or whose child controls modify the gallery's Items in OnChange / OnSelect — nested galleries cause performance issues and potential infinite loops.
  • Flag ComboBox, Slider, DatePicker, or Toggle controls inside galleries — their OnChange fires unexpectedly when data changes.

4.2 Responsive Layout & Containers

  • Flag apps with Scale to Fit enabled — this only scales, it doesn't adapt; recommend disabling and using formula-based positioning.
  • Flag controls with fixed pixel coordinates instead of formula-based sizing (Parent.Width, Parent.Height).
  • Flag fixed-height galleries — recommend flexible height (Height = Parent.Height).
  • Flag controls positioned absolutely without a parent Container — 🟢 Low:
    • Groups (legacy) have no individual properties, cannot be nested logically, and are invisible to screen readers.
    • Container controls have their own Width, Height, BorderColor, and Fill properties; support nesting; and create a logical structure that screen readers and accessibility tools can interpret.
    • Recommend replacing groups with containers for all layout grouping beyond purely visual convenience.

4.3 Modern Controls & Components

  • Flag classic controls used in new screens where a Modern Control (Fluent Design) equivalent exists — modern controls provide built-in theming, accessibility, and reduced custom code.
  • Flag large groups of theme Set() variables in App.OnStart that could be eliminated by adopting modern controls with an app theme.
  • Identify duplicate control groups (e.g., navigation bar rebuilt on every screen) — extract into a canvas component.

4.4 Form Design

  • Flag apps with separate Create, Edit, and View forms for the same entity — recommend a single form with dynamic FormMode switching (FormMode.New / FormMode.Edit / FormMode.View).
  • Flag forms with no unsaved-data protection on navigation.

5. Audit — Error Handling

5.1 Missing Formula-Level Error Handling

  • Flag Patch(), SubmitForm(), Remove(), and connector calls with no IfError(...) wrapper and no user notification on failure.
    // GOOD
    IfError(
        Patch(Orders, Defaults(Orders), { Status: "New" }),
        Notify("Save failed: " & FirstError.Message, NotificationType.Error)
    )
    

5.2 Missing Form OnFailure Handler

  • Flag SubmitForm(frm...) buttons where the form's OnFailure property is empty — users receive no feedback when submission fails. Recommend: OnFailure = Notify("Error: " & Form.Error, NotificationType.Error)

5.3 Missing App.OnError

  • Flag apps where App.OnError is empty — unhandled errors show raw system messages to users and are not logged.
    // GOOD — centralized error logging
    Notify(
        Concatenate("Error: ", FirstError.Message,
            " | Source: ", FirstError.Source),
        NotificationType.Error
    )
    

5.4 Observability Recommendations

  • Check the App object for an Application Insights connection string property:
    • If not set: flag as 🟡 Medium — recommend connecting to Application Insights for session telemetry, TTI/TTFL trends, and error visibility (see Monitoring section of report for setup steps).
    • If set: flag the following two as 🟢 Low advisory improvements:
      1. Unhandled Error Monitoring (experimental): in Power Apps Studio → Settings → Updates → Experimental → enable "Pass errors to Azure Application Insights". This logs all unhandled Power Fx runtime errors to the traces table with the message "Unhandled error", enabling proactive alerting without waiting for user reports.
        traces
        | where message == "Unhandled error"
        | extend d = parse_json(customDimensions)
        | extend errors = parse_json(tostring(d.errors))
        | mv-expand errors
        | project timestamp, AppName = d['ms-appName'], ErrorMessage = errors.Message
        | order by timestamp desc
        
      2. Correlation Tracing (experimental, custom connectors only): in Settings → Updates → Experimental → enable "Enable Azure Application Insights correlation tracing". Propagates W3C trace context to custom connectors, enabling end-to-end distributed tracing when the downstream service is also connected to App Insights. ⚠️ Works only with custom connectors — standard connectors are not supported.


6. Audit — App Checker & Accessibility

Process the raw results collected in Step 1 from get_appchecker_errors and get_accessibility_errors.

6.1 App Checker Errors & Warnings

For each item returned by get_appchecker_errors:

  • Map the built-in severity from the tool to the report scale:
    • Error → 🔴 High
    • Warning → 🟡 Medium
    • Suggestion / Info → 🟢 Low
  • Record: Category = App Checker, Area = the checker rule name or category (e.g., Formula Error, Performance, Data Source, Deprecated), Location = screen + control path as returned by the tool, Code Found = the offending formula snippet (≤ 120 chars), Finding = the checker message verbatim, Recommendation = actionable fix.
  • Do NOT duplicate findings already captured in sections 2–5 for the same location and formula — add a note (also flagged in Section [N]) instead of a full duplicate row.
  • If get_appchecker_errors returns no issues, record a single 🟢 Low informational row: "App Checker returned no errors or warnings. The app is clean per the checker."

6.2 Accessibility Errors

For each item returned by get_accessibility_errors:

  • Map severity:
    • Error → 🔴 High
    • Warning → 🟡 Medium
    • Suggestion → 🟢 Low
  • Record: Category = Accessibility, Area = the accessibility rule (e.g., Missing Label, Color Contrast, Tab Order, Screen Reader, Keyboard Navigation), Location = screen + control name, Code Found = the property value that caused the issue (e.g., the empty AccessibleLabel value), Finding = the checker message verbatim, Recommendation = actionable fix.
  • If get_accessibility_errors returns no issues, record a single 🟢 Low informational row: "Accessibility Checker returned no errors. The app meets baseline accessibility standards."

7. Query & Data Source Analysis

Analyze all data access patterns in the app. Read .pa.yaml files only — do not modify them.

7.1 App.OnStart Query Load Time Estimation

Read the OnStart property from App.pa.yaml and identify every query against an external data source (Dataverse, SharePoint, SQL, connector call, Power Automate flow). Classify each query by its execution context:

  • Sequential top-level: each counts as 100 ms.
  • Inside Concurrent(): the entire Concurrent block counts as 100 ms (all queries inside fire in parallel — the block's cost is the slowest single query, approximated as 100 ms).
  • Inside an If() condition: the query may or may not run at runtime. Track separately as a conditional query.
  • Inside a ForAll() loop: mark as N+1. Each distinct query call inside the loop costs 100 ms per iteration. Report for the best case (loop runs exactly once).

Produce three KPI values to render in the HTML report:

KPICalculation Rule
Best-Case Start TimeSum 100 ms per sequential query and per Concurrent block that is not inside any If() branch (conditional queries assumed false — skipped).
Worst-Case Start TimeSum 100 ms per sequential query and per Concurrent block including those inside If() branches (all branches assumed true).
ForAll N+1 Risk (1 iteration)For each ForAll that contains external queries: sum 100 ms × number of distinct query calls inside, assuming the loop runs exactly once. Note in the report that actual runtime cost = this value × N records.

7.2 Per-Screen Data Source Operations & Duplicate Detection

For each screen .pa.yaml file (skip App.pa.yaml — it is not a screen):

  • Enumerate every Filter(), LookUp(), Search(), ClearCollect(), Collect() call against an external data source, plus any connector or Power Automate flow call.
  • Record: Screen, Control.Property, Data Source, Operation (Filter / LookUp / Search / ClearCollect / Collect / Connector / Flow), Filter Condition (exact predicate text extracted from the formula).
  • Duplicate detection: within each screen, group entries by data source. Flag any group where two or more queries share the same data source with the same or structurally similar filter condition (same field names and operators; literal values may differ):
    • 🔴 Identical — same data source, same filter condition: redundant HTTP call. Severity: High.
    • 🟡 Near-Duplicate — same data source, filter differs only in literal values: consolidation candidate. Severity: Medium.

7.3 Named Formula Query & Connector Usage Per Screen

From App.pa.yamlApp.Formulas, identify every named formula that contains:

  • A query against an external data source (Filter, LookUp, Search, ClearCollect, Collect), or
  • A connector method call (e.g., Office365Users.*, MSN.*, any custom connector method), or
  • A Power Automate flow call.

For each qualifying named formula:

  1. Search all screen .pa.yaml files for references to this formula name.
  2. Count how many times it is referenced per screen.
  3. Report: Formula Name, Data Source / Connector / Flow Referenced, per-screen usage count.

7.4 Repeated Queries Per Screen

For each screen, identify pairs or groups of queries where:

  • Both/all queries target the same external data source, AND
  • Filter conditions are identical or differ only in a literal value (e.g., Status = "Active" vs Status = "Inactive").

Flag 🔴 High for identical duplicate queries (pure redundant round-trips). Flag 🟡 Medium for near-duplicates (consolidation opportunity). Include the control.property and formula snippet for each query in the group.

7.5 Wide-Column Queries

Identify every query that retrieves records without restricting columns — no ShowColumns() wrapper and no ECS-enforced explicit column selection — from an external data source. For each:

  • Record: location (screen name or App.OnStart / App.Formulas), control.property, exact query code (≤ 120 chars), data source name, estimated columns returned.
  • Estimate column count:
    • If the data source schema is visible in the YAML (e.g., Dataverse column metadata listed), count the columns.
    • Otherwise record: schema not determinable from YAML — all columns returned.
  • Sort results descending by estimated column count.
  • Severity: 🔴 High if estimated columns ≥ 20 · 🟡 Medium if 10–19 · 🟢 Low if < 10 (but ShowColumns() still recommended).

8. Generate HTML Report File

After completing all audits, determine the output filename:

  1. Try Performance & Quality Review.html first.
  2. If a file with that name already exists in the same directory as the .pa.yaml files, append a timestamp: Performance & Quality Review_YYYYMMDD_HHmmss.html (e.g. Performance & Quality Review_20260419_143022.html).

Write the report using the template below, replacing all {{PLACEHOLDER}} tokens with real data.

Then post a brief in-chat confirmation:

Report saved: [resolved filename] 🔴 High: [N] · 🟡 Medium: [N] · 🟢 Low: [N] · Total findings: [N] Open the file in a browser, then copy-paste the content into Outlook to share.


HTML Template

For the Code Found column extract the exact formula or property value from the .pa.yaml file (≤ 120 characters). Use <code> tags for inline formulas. For missing/empty properties write <em>(property empty)</em>. For structural issues write the count, e.g. <em>(62 controls)</em>.

For severity badges use:

  • High → <span style="background:#C50F1F;color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">High</span>
  • Medium → <span style="background:#F7630C;color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Medium</span>
  • Low → <span style="background:#107C10;color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Low</span>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Canvas App Audit Report — {{APP_NAME}}</title>
</head>
<body style="margin:0;padding:0;background-color:#f3f2f1;font-family:'Segoe UI',Arial,sans-serif;font-size:14px;color:#323130;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f3f2f1;">
  <tr><td align="center" style="padding:32px 16px;">
  <table width="800" cellpadding="0" cellspacing="0" style="max-width:800px;background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.12);">

    <!-- HEADER -->
    <tr>
      <td style="background-color:#742774;padding:32px 40px;">
        <table width="100%" cellpadding="0" cellspacing="0">
          <tr>
            <td>
              <p style="margin:0 0 4px 0;font-size:11px;color:#e6c9e6;letter-spacing:1px;text-transform:uppercase;">Power Apps · Canvas App</p>
              <h1 style="margin:0 0 8px 0;font-size:26px;font-weight:700;color:#ffffff;">Performance &amp; Quality Audit</h1>
              <p style="margin:0;font-size:18px;color:#e6c9e6;font-weight:400;">{{APP_NAME}}</p>
            </td>
            <td align="right" valign="top">
              <p style="margin:0;font-size:12px;color:#d4a8d4;">{{REPORT_DATE}}</p>
              <p style="margin:4px 0 0 0;font-size:12px;color:#d4a8d4;">Audited by: {{AUTHOR}}</p>
            </td>
          </tr>
        </table>
      </td>
    </tr>

    <!-- SCORE CARDS -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <table width="100%" cellpadding="0" cellspacing="0">
          <tr>
            <td style="padding-right:12px;">
              <table width="100%" cellpadding="16" style="background:#fde7e9;border-radius:6px;border-left:4px solid #C50F1F;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#842029;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">High Severity</p>
                  <p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#C50F1F;">{{TOTAL_HIGH}}</p>
                </td></tr>
              </table>
            </td>
            <td style="padding-right:12px;">
              <table width="100%" cellpadding="16" style="background:#fff4ce;border-radius:6px;border-left:4px solid #F7630C;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#7a4100;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Medium Severity</p>
                  <p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#F7630C;">{{TOTAL_MEDIUM}}</p>
                </td></tr>
              </table>
            </td>
            <td style="padding-right:12px;">
              <table width="100%" cellpadding="16" style="background:#dff6dd;border-radius:6px;border-left:4px solid #107C10;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#0e4a0b;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Low Severity</p>
                  <p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#107C10;">{{TOTAL_LOW}}</p>
                </td></tr>
              </table>
            </td>
            <td>
              <table width="100%" cellpadding="16" style="background:#ede0f5;border-radius:6px;border-left:4px solid #742774;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#4a1a5c;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Total Findings</p>
                  <p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#742774;">{{TOTAL_FINDINGS}}</p>
                </td></tr>
              </table>
            </td>
          </tr>
        </table>
      </td>
    </tr>

    <!-- APP OVERVIEW -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">App Overview</h2>
        <table width="100%" cellpadding="8" cellspacing="0" style="border-collapse:collapse;font-size:13px;">
          <tr style="background:#f3f2f1;">
            <td style="padding:8px 12px;font-weight:600;width:200px;">Screens</td>
            <td style="padding:8px 12px;">{{SCREEN_COUNT}}</td>
            <td style="padding:8px 12px;font-weight:600;width:200px;">Total Controls</td>
            <td style="padding:8px 12px;">{{CONTROL_COUNT}}</td>
          </tr>
          <tr>
            <td style="padding:8px 12px;font-weight:600;">Data Sources</td>
            <td style="padding:8px 12px;" colspan="3">{{DATA_SOURCES}}</td>
          </tr>
          <tr style="background:#f3f2f1;">
            <td style="padding:8px 12px;font-weight:600;">Flow Calls</td>
            <td style="padding:8px 12px;">{{FLOW_CALLS}}</td>
            <td style="padding:8px 12px;font-weight:600;">Connectors</td>
            <td style="padding:8px 12px;">{{CONNECTORS}}</td>
          </tr>
        </table>
      </td>
    </tr>

    <!-- CATEGORY SUMMARY -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">Summary by Category</h2>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:13px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:10px 12px;text-align:left;font-weight:600;">Category</th>
            <th style="padding:10px 12px;text-align:center;font-weight:600;">🔴 High</th>
            <th style="padding:10px 12px;text-align:center;font-weight:600;">🟡 Medium</th>
            <th style="padding:10px 12px;text-align:center;font-weight:600;">🟢 Low</th>
          </tr>
          <!-- Repeat one <tr> per category. Alternate row background: #ffffff / #f3f2f1 -->
          <tr style="background:#ffffff;">
            <td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Performance</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{PERF_HIGH}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{PERF_MED}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{PERF_LOW}}</td>
          </tr>
          <tr style="background:#f3f2f1;">
            <td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Coding Standards</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CODE_HIGH}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CODE_MED}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CODE_LOW}}</td>
          </tr>
          <tr style="background:#ffffff;">
            <td style="padding:9px 12px;border-bottom:1px solid #edebe9;">App Design</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{DESIGN_HIGH}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{DESIGN_MED}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{DESIGN_LOW}}</td>
          </tr>
          <tr style="background:#f3f2f1;">
            <td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Error Handling</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{ERR_HIGH}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{ERR_MED}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{ERR_LOW}}</td>
          </tr>
          <tr style="background:#ffffff;">
            <td style="padding:9px 12px;border-bottom:1px solid #edebe9;">App Checker</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CHECKER_HIGH}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CHECKER_MED}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CHECKER_LOW}}</td>
          </tr>
          <tr style="background:#f3f2f1;">
            <td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Accessibility</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{A11Y_HIGH}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{A11Y_MED}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{A11Y_LOW}}</td>
          </tr>
          <tr style="background:#ffffff;">
            <td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Query Analysis</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{QUERY_HIGH}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{QUERY_MED}}</td>
            <td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{QUERY_LOW}}</td>
          </tr>
        </table>
      </td>
    </tr>

    <!-- ALL FINDINGS -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">All Findings</h2>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:28px;">#</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:72px;">Severity</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:100px;">Category</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Area</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:130px;">Location</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:140px;">Code Found</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;">Finding &amp; Recommendation</th>
          </tr>
          <!-- For each finding generate one <tr>. Alternate background #ffffff / #f9f8f7.
               For High rows add style="border-left:3px solid #C50F1F;" to the first <td>.
               For Medium rows add style="border-left:3px solid #F7630C;" to the first <td>.
               For Low rows add style="border-left:3px solid #107C10;" to the first <td>. -->
          <!-- EXAMPLE ROW (repeat for every finding):
          <tr style="background:#ffffff;">
            <td style="padding:8px 10px;border-bottom:1px solid #edebe9;border-left:3px solid #C50F1F;vertical-align:top;">1</td>
            <td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;"><span style="background:#C50F1F;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;">High</span></td>
            <td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;">Performance</td>
            <td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;">N+1 Calls</td>
            <td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;">Orders Screen — galOrders.Items</td>
            <td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;font-family:Consolas,monospace;font-size:11px;color:#323130;background:#f3f2f1;">LookUp(Users, ID=ThisItem.CreatedBy)</td>
            <td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;"><strong>Finding:</strong> Each gallery row triggers a separate LookUp call (N+1).<br><strong>Fix:</strong> Use the Dataverse relationship: <code>ThisItem.'Created By'.'Full Name'</code></td>
          </tr>
          -->
        </table>
      </td>
    </tr>

    <!-- APP CHECKER RESULTS -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">🔍 App Checker Results</h2>
        <p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Issues detected by the Power Apps App Checker engine (formula errors, performance warnings, deprecated features, data source issues).</p>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:28px;">#</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:72px;">Severity</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Rule / Area</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:160px;">Location</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:150px;">Code Found</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;">Message &amp; Recommendation</th>
          </tr>
          <!-- For each App Checker finding generate one <tr> using the same row pattern as All Findings.
               Category column is omitted here since all rows are "App Checker".
               If no issues: single row with green badge and "App Checker returned no errors or warnings." -->
          {{APPCHECKER_ROWS}}
        </table>
      </td>
    </tr>

    <!-- ACCESSIBILITY RESULTS -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">♿ Accessibility Checker Results</h2>
        <p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Issues detected by the Power Apps Accessibility Checker (labels, color contrast, tab order, screen reader support, keyboard navigation).</p>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:28px;">#</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:72px;">Severity</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:130px;">Rule / Area</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:160px;">Control</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:150px;">Property Value</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;">Message &amp; Recommendation</th>
          </tr>
          <!-- For each Accessibility finding generate one <tr>.
               If no issues: single row with green badge and "Accessibility Checker returned no errors." -->
          {{ACCESSIBILITY_ROWS}}
        </table>
      </td>
    </tr>

    <!-- QUERY ANALYSIS -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 16px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">🗃️ Query &amp; Data Source Analysis</h2>

        <!-- OnStart Load Time KPIs -->
        <h3 style="margin:0 0 8px 0;font-size:14px;font-weight:600;color:#323130;">App.OnStart — Estimated Load Time</h3>
        <p style="margin:0 0 14px 0;font-size:12px;color:#605e5c;">Each external query is estimated at 100 ms. Queries inside <code>Concurrent()</code> count as a single 100 ms block regardless of how many queries are inside. Queries inside <code>ForAll()</code> are reported separately as N+1 risks.</p>
        <table width="100%" cellpadding="0" cellspacing="0">
          <tr>
            <td style="padding-right:12px;" width="33%">
              <table width="100%" cellpadding="16" style="background:#dff6dd;border-radius:6px;border-left:4px solid #107C10;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#0e4a0b;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">✅ Best-Case Start Time</p>
                  <p style="margin:6px 0 4px 0;font-size:28px;font-weight:700;color:#107C10;">{{ONSTART_BEST_MS}} ms</p>
                  <p style="margin:0;font-size:11px;color:#0e4a0b;">All <code>If()</code> branches assumed <strong>false</strong> — conditional queries are skipped</p>
                </td></tr>
              </table>
            </td>
            <td style="padding-right:12px;" width="33%">
              <table width="100%" cellpadding="16" style="background:#fff4ce;border-radius:6px;border-left:4px solid #F7630C;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#7a4100;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">⚠️ Worst-Case Start Time</p>
                  <p style="margin:6px 0 4px 0;font-size:28px;font-weight:700;color:#F7630C;">{{ONSTART_WORST_MS}} ms</p>
                  <p style="margin:0;font-size:11px;color:#7a4100;">All <code>If()</code> branches assumed <strong>true</strong> — every conditional query runs</p>
                </td></tr>
              </table>
            </td>
            <td width="33%">
              <table width="100%" cellpadding="16" style="background:#fde7e9;border-radius:6px;border-left:4px solid #C50F1F;">
                <tr><td>
                  <p style="margin:0;font-size:11px;color:#842029;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">🔄 ForAll N+1 Risk (1 iteration)</p>
                  <p style="margin:6px 0 4px 0;font-size:28px;font-weight:700;color:#C50F1F;">{{ONSTART_FORALL_MS}} ms</p>
                  <p style="margin:0;font-size:11px;color:#842029;">Loop runs <strong>once</strong> (best case) — actual cost multiplies with record count (N × this value)</p>
                </td></tr>
              </table>
            </td>
          </tr>
        </table>

        <!-- OnStart Query Breakdown Table -->
        <h3 style="margin:22px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">App.OnStart Query Breakdown</h3>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:32px;">#</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;">Query / Formula (≤ 120 chars)</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:120px;">Data Source</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:120px;">Execution Context</th>
            <th style="padding:9px 10px;text-align:center;font-weight:600;width:90px;">Est. Time</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:100px;">Counted In KPI</th>
          </tr>
          <!-- For each query in App.OnStart generate one <tr>. Alternate background #ffffff / #f9f8f7.
               Execution Context values: Sequential | Concurrent (block N) | If — <condition> | ForAll
               Counted In KPI: Best + Worst | Worst Only (inside If) | N+1 Only (inside ForAll) -->
          {{ONSTART_QUERY_ROWS}}
        </table>

        <!-- Per-Screen Data Source Operations & Duplicates -->
        <h3 style="margin:28px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">Data Source Operations &amp; Duplicates per Screen</h3>
        <p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">All Filter, LookUp, Search, ClearCollect, Collect, connector, and flow calls found in each screen's control formulas. Queries to the same data source within a screen are checked for identical or near-duplicate conditions.</p>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:32px;">#</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:100px;">Screen</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:140px;">Control.Property</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Data Source</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:80px;">Operation</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;">Filter Condition</th>
            <th style="padding:9px 10px;text-align:center;font-weight:600;width:100px;">Duplicate?</th>
          </tr>
          <!-- For each operation generate one <tr>. Alternate background #ffffff / #f9f8f7.
               Duplicate column: "🔴 Identical" | "🟡 Near-Duplicate" | "—" -->
          {{DATASOURCE_OPS_ROWS}}
        </table>

        <!-- Named Formula Usage Per Screen -->
        <h3 style="margin:28px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">Named Formulas with Queries, Connectors &amp; Flows — Usage per Screen</h3>
        <p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Named formulas defined in <code>App.Formulas</code> (in <code>App.pa.yaml</code>) that access external data or services, and how many times each is referenced across screens.</p>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:150px;">Formula Name</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:140px;">Data Source / Connector / Flow</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:80px;">Operation</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;">Usage per Screen (screen: count, …)</th>
            <th style="padding:9px 10px;text-align:center;font-weight:600;width:80px;">Total Uses</th>
          </tr>
          <!-- For each qualifying named formula generate one <tr>. Alternate background #ffffff / #f9f8f7.
               Usage per Screen cell format: "Home Screen: 3, Orders Screen: 1" -->
          {{NAMED_FORMULA_ROWS}}
        </table>

        <!-- Repeated Queries Per Screen -->
        <h3 style="margin:28px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">Repeated Queries per Screen</h3>
        <p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Queries to the same data source within a screen where filter conditions are identical or differ only in literal values. These are candidates for consolidation into a shared collection or parameterized formula.</p>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:32px;">#</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:100px;">Screen</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Data Source</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:80px;">Severity</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;">Queries Found (Control.Property — Filter Condition)</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:170px;">Recommendation</th>
          </tr>
          <!-- For each repeated-query group generate one <tr>. Alternate background #ffffff / #f9f8f7.
               Severity badge: 🔴 Identical (redundant) | 🟡 Near-Duplicate (consolidate)
               List each query in the group as a separate line inside the Queries Found cell. -->
          {{REPEATED_QUERY_ROWS}}
        </table>

        <!-- Wide-Column Queries -->
        <h3 style="margin:28px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">Wide-Column Queries — Most Columns Returned (Descending)</h3>
        <p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Queries returning all or many columns without <code>ShowColumns()</code> restriction or ECS column selection. Restricting columns reduces payload size and improves load time.</p>
        <table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
          <tr style="background:#742774;color:#ffffff;">
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:32px;">#</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:72px;">Severity</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Location</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;width:140px;">Control.Property</th>
            <th style="padding:9px 10px;text-align:left;font-weight:600;">Query (≤ 120 chars)</th>
            <th style="padding:9px 10px;text-align:center;font-weight:600;width:90px;">Est. Columns ↓</th>
          </tr>
          <!-- For each wide-column query generate one <tr>, sorted descending by Est. Columns.
               Severity: High (≥ 20 cols) | Medium (10–19) | Low (< 10)
               If schema not determinable write: "unknown — all columns"
               Alternate background #ffffff / #f9f8f7 -->
          {{WIDE_COLUMN_ROWS}}
        </table>

      </td>
    </tr>

    <!-- QUICK WINS -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <table width="100%" cellpadding="16" cellspacing="0" style="background:#dff6dd;border-radius:6px;border-left:4px solid #107C10;">
          <tr><td>
            <h2 style="margin:0 0 10px 0;font-size:15px;font-weight:600;color:#0e4a0b;">⚡ Quick Wins — Fix in &lt; 5 Minutes</h2>
            <!-- For each 🟢 Low / easy 🟡 Medium finding add a bullet: -->
            <ul style="margin:0;padding-left:20px;line-height:1.7;">
              <!-- <li><strong>[Area]</strong> — [one-line recommendation]</li> -->
              {{QUICK_WINS_LIST}}
            </ul>
          </td></tr>
        </table>
      </td>
    </tr>

    <!-- HIGHEST IMPACT -->
    <tr>
      <td style="padding:20px 40px 0 40px;">
        <table width="100%" cellpadding="16" cellspacing="0" style="background:#fde7e9;border-radius:6px;border-left:4px solid #C50F1F;">
          <tr><td>
            <h2 style="margin:0 0 10px 0;font-size:15px;font-weight:600;color:#842029;">🔴 Highest Impact Changes</h2>
            <ul style="margin:0;padding-left:20px;line-height:1.7;">
              <!-- <li><strong>[Area]</strong> — [one-line recommendation]</li> -->
              {{HIGH_IMPACT_LIST}}
            </ul>
          </td></tr>
        </table>
      </td>
    </tr>

    <!-- MONITORING -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">📊 How to Monitor This App Going Forward</h2>
        <p style="margin:0 0 10px 0;font-weight:600;color:#323130;">Option 1 — Monitor in Power Apps (PPAC)</p>
        <ol style="margin:0 0 16px 0;padding-left:20px;line-height:1.8;color:#323130;">
          <li>Sign in to <a href="https://make.powerapps.com" style="color:#742774;">make.powerapps.com</a> and select <strong>Apps</strong>.</li>
          <li>On the command bar, select <strong>Monitor</strong> and pin it to the left nav.</li>
          <li>The Monitor page shows the top 3 underperforming apps per metric. Key metrics: <strong>Time to Interactive (TTI)</strong>, <strong>Time to Full Load (TTFL)</strong>, <strong>App Open Success Rate</strong>, <strong>Data Request Latency</strong>.</li>
          <li>Select the app to open a <strong>30-day trend side panel</strong>. In Managed Environments, a recommended action callout appears on each metric chart.</li>
        </ol>
        <p style="margin:0 0 10px 0;font-weight:600;color:#323130;">Option 2 — Azure Application Insights</p>
        <ol style="margin:0 0 8px 0;padding-left:20px;line-height:1.8;color:#323130;">
          <li>Create a <a href="https://learn.microsoft.com/en-us/azure/azure-monitor/app/create-workspace-resource" style="color:#742774;">workspace-based Application Insights resource</a> in Azure portal.</li>
          <li>Copy the <strong>Connection string</strong> and paste it into the <strong>App</strong> object in Power Apps Studio.</li>
          <li><strong>Save &amp; Publish</strong> the app — events only stream from the published app, not Studio Preview.</li>
          <li>In Azure portal → App Insights → <strong>Logs</strong>, run KQL queries for success rate, TTI/TTFL percentile trends, slowest TTFL-blocking HTTP calls, and failed launch reasons.</li>
        </ol>
      </td>
    </tr>

    <!-- LEARNING — PATTERN EXPLANATIONS -->
    <tr>
      <td style="padding:28px 40px 0 40px;">
        <h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">📚 Understanding the Patterns Found in This App</h2>
        <p style="margin:0 0 14px 0;color:#605e5c;font-size:13px;">Power Apps is designed to guide makers toward performant patterns by default. The following explains each issue category detected in this report.</p>
        <!-- For each category that has at least one finding, generate a block: -->
        <!-- EXAMPLE BLOCK:
        <table width="100%" cellpadding="14" cellspacing="0" style="background:#f3f2f1;border-radius:6px;margin-bottom:10px;">
          <tr><td>
            <p style="margin:0 0 4px 0;font-weight:600;color:#323130;">[Pattern Name]</p>
            <p style="margin:0 0 6px 0;color:#323130;line-height:1.6;">[2-3 sentence plain-language explanation: what it is, why it hurts, what the correct pattern looks like.]</p>
            <p style="margin:0;"><a href="[MS docs URL]" style="color:#742774;font-size:12px;">Learn more →</a></p>
          </td></tr>
        </table>
        -->
        {{PATTERN_EXPLANATIONS}}
      </td>
    </tr>

    <!-- FOOTER -->
    <tr>
      <td style="padding:28px 40px;margin-top:28px;background:#f3f2f1;border-top:1px solid #edebe9;">
        <table width="100%" cellpadding="0" cellspacing="0">
          <tr>
            <td style="font-size:11px;color:#a19f9d;">Generated by {{AUTHOR}} · GitHub Copilot · analyze-canvas-performance skill · {{REPORT_DATE}}</td>
            <td align="right" style="font-size:11px;color:#a19f9d;">
              <a href="https://learn.microsoft.com/en-us/power-apps/maker/canvas-apps/performance-tips" style="color:#742774;text-decoration:none;">Performance Tips</a> ·
              <a href="https://learn.microsoft.com/en-us/power-apps/guidance/coding-guidelines/code-optimization" style="color:#742774;text-decoration:none;">Code Optimization</a>
            </td>
          </tr>
        </table>
      </td>
    </tr>

  </table>
  </td></tr>
</table>
</body>
</html>

8. Offer to Apply Fixes

Would you like me to apply any of these recommendations?

  • Apply all High severity fixes
  • Let me choose which fixes to apply
  • No changes — report only

If the user approves changes, edit the .pa.yaml files following conventions from ${CLAUDE_PLUGIN_ROOT}/references/TechnicalGuide.md, then call compile_canvas to validate. Fix any compilation errors before finishing.