代码优化

随着画布应用的演进以满足不同的业务需求,保持性能优化至关重要。 数据处理、用户界面设计和应用功能都需要谨慎的代码优化方法。

当画布应用变得更加复杂时,可能会遇到数据检索、公式复杂性和渲染速度等问题。 在强大的功能和响应迅速的用户界面之间取得平衡意味着您需要一种系统的代码优化方法。

Power Fx 公式优化

With 函数

With 函数计算单个记录的公式。 公式可以计算值或执行操作,例如修改数据或处理连接。 使用 With 将复杂公式拆分为较小的命名子公式,使其更易于阅读。 这些命名值类似于仅限于 With 作用域的简单局部变量。 使用 With 优于上下文或全局变量,因为它是自包含的、易于理解,并在任何声明式公式上下文中均可使用。

使用 With 函数的 Power Fx 公式的屏幕截图

Concurrent 函数

Concurrent 函数允许同一属性中的多个公式在存在连接器或 Dataverse 调用时同时评估。 通常,当您使用 ;(分号)运算符将多个公式串联时,它们会同时被评估。 使用 Concurrent 时,应用程序会同时评估属性中的所有公式,即使在使用 ; 运算符之后也是如此。 这种并发性意味着用户等待结果的时间更少。 如果必须等到前一个调用完成才能启动数据调用,则应用必须等待所有请求时间之和。 如果数据调用都同时启动,应用只需等待最长请求的时间。

Concurrent(
    ClearCollect(colAccounts1, Accounts),
    ClearCollect(colUsers1, Users),
    ClearCollect(colEnvDef1, 'Environment Variable Definitions'),
    ClearCollect(colEnvVal1, 'Environment Variable Values')
);

Coalesce 函数

Coalesce 函数按顺序对参数求值并返回第一个值(不是空白或空字符串)。 使用此函数将空白值或空字符串替换为其他值,但保留非空白和非空字符串值不变。 如果所有参数均为空或空字符串,该函数将返回空值。 Coalesce 是将空字符串转换为空值的好方法。

例如:

If(Not IsBlank(value1), value1, Not IsBlank(value2), value2)

要求对值 1 和值 2 进行评估两次。 该函数可简化为:

Coalesce(value1, value2)

IsMatch 函数

IsMatch 函数用于检测文本字符串是否与由普通字符、预定义模式或正则表达式组成的模式匹配。

例如,此函数匹配美国社会安全号码:

IsMatch(TextInput1.Text, "\d{3}-\d{2}-\d{4}")

正则表达式的说明:

\\d 匹配任何数字 (0-9)。

{3} 指定前面的数字模式 (\d) 应只出现三次。

- 匹配连字符。

{2} 指定前面的数字模式 (\d) 应只出现两次。

{4} 指定前面的数字模式 (\d) 应只出现四次。

IsMatch 的更多示例:

IsMatch(TextInput1.Text, "Hello World")
IsMatch(TextInput1\_2.Text, "(?!^\[0-9\]\\\*$)(?!^\[a-zA-Z\]\\\*$)(\[a-zA-Z0-9\]{8,10})")

优化应用 OnStart

画布应用程序的 OnStart 属性在定义应用程序启动时发生的操作方面起着至关重要的作用。 此属性允许应用程序开发人员执行全局初始化任务、设置变量以及执行在应用程序启动过程中只发生一次的操作。 理解并有效利用 OnStart 属性对于创建响应迅速且高效的画布应用程序至关重要。

推荐的方法是通过将变量设置迁移到命名公式来简化 App.OnStart 函数。 命名公式,尤其是在应用生命周期早期配置的公式,被证明是有利的。 这些公式根据数据调用处理变量的初始化,这为代码提供了更整洁、更组织的结构。 更多详细信息构建复杂的大型画布应用 - Power Apps | Microsoft Learn

备注

OnStart 属性是命令性。这是第一个屏幕显示前需要完成的工作顺序列表。 因为它不仅明确指出需要做什么,而且还明确说明何时必须根据顺序完成工作,因此限制了原本可以完成的重新排序和延迟优化。

启动屏幕

如果 App.OnStart 包含 Navigate 函数调用,即使它在 If 函数中并且很少被调用,我们也必须完成应用程序的执行。 OnStart 我们显示应用程序的第一个屏幕之前。  App.StartScreen 是一种新的声明性方法,用于指示应首先显示哪个屏幕,这不会阻止优化。

设置 StartScreen 属性会在 App.OnStart 完成之前显示第一个屏幕。 App.StartScreen declares 首先显示哪个屏幕对象,无需任何预处理。

而不是编写像下面这样的代码:

App.OnStart = Collect(OrdersCache, Orders);
If(Param("AdminMode") = "1", Navigate(AdminScreen), Navigate(HomeScreen))

将代码更改为:

App.OnStart = Collect(OrdersCache, Orders);
App.StartScreen = If(Param("AdminMode") = "1", AdminScreen, HomeScreen)

详情请参阅 <https://Power Apps.microsoft.com/en-us/blog/app-startscreen-a-new-declarative-alternative-to-navigate-in-app-on-start/>。

警告

避免 StartScreenOnStart 之间的依赖关系。 引用一个命名公式,而这个命名公式又引用一个全局变量,这可能会导致一种竞争情况,在这种情况下,StartScreen 不能正确应用。 注意:在 StartScreen 和 OnStart 之间不应存在依赖项。 我们阻止在 StartScreen 中引用全局变量,但我们可能引用一个命名公式,而这个命名公式又引用一个全局变量,这可能会导致一种竞争情况,在这种情况下,StartScreen 无法正确应用。

命名公式

命名公式是可在 App.Formulas 部分定义的静态或常量。 在 App.Formulas 中进行声明后,就可以在应用程序的任何地方使用,并且其值始终保持最新。 通过 Power Apps 中的命名公式,可以定义由平台自动管理和更新的值或值集合。 这一功能可将数值计算和维护的责任从开发人员转移给 Power Apps,从而简化开发流程。 中的命名公式 Power Apps 是一项强大功能,可以显著增强应用程序性能和可维护性。

命名公式还可用于声明应用程序主题。 在构建企业应用程序的许多情况下,我们希望应用程序有共同的主题,以提供一致的外观和用户体验。 要创建一个主题,需要在 App OnStart 上声明 10 到 100 个变量。 这增加了应用程序的代码长度和初始化时间。

现代控件也能为主题创建提供很大帮助,并有助于减少客户为处理主题而编写的逻辑。 现代控件当前处于预览状态。

例如,可将 App.OnStart 上的以下代码移动至 App.Formulas,从而减少全局变量声明的启动时间。

Set(BoardDark, RGBA(181,136,99, 1));
Set(BoardSelect, RGBA(34,177,76,1));
Set(BoardRowWidth, 10);                      // expected 8 plus two guard characters for regular expressions.
Set(BoardMetadata, 8 \* BoardRowWidth + 1);   // which player is next, have pieces moved for castling rules, etc.
Set(BoardBlank, "----------------------------------------------------------------\_00000000000000");
Set(BoardClassic, "RNBQKBNR\_\_PPPPPPPP------------------------\_--------\_\_pppppppp\_\_rnbqkbnr\_\_0000000000");

可以将代码移动到 App.Formulas,如下所示:

BoardSize = 70;
BoardLight = RGBA(240,217,181, 1);
BoardDark = RGBA(181,136,99, 1);
BoardSelect = RGBA(34,177,76,1);
BoardRowWidth = 10;                      // expected 8 plus two guard characters for regular expressions
BoardMetadata = 8 \* BoardRowWidth + 1;   // which player is next, have pieces moved for castling rules, etc.
BoardBlank = "----------------------------------------------------------------\_00000000000000";
BoardClassic = "RNBQKBNR\_\_PPPPPPPP------------------------\_--------\_\_pppppppp\_\_rnbqkbnr\_\_0000000000";

另一个例子是设置 Lookups。 这里需要修改 Lookup 公式,以便从 Office 365 而不是 Dataverse 中获取用户信息。 只有一个地方需要修改,而不需要修改所有地方的代码。

UserEmail = User().Email;
UserInfo = LookUp(Users, 'Primary Email' = User().Email);
UserTitle = UserInfo.Title;
UserPhone = Switch(UserInfo.'Preferred Phone', 'Preferred Phone (Users)'.'Mobile Phone', UserInfo.'Mobile Phone',
UserInfo.'Main Phone');

这些公式体现了计算的本质。 它们阐明了根据其他值确定 UserEmailUserInfoUserTitleUserPhone 的过程。 此逻辑进行封装后,可在整个应用程序中广泛使用,并可在单一位置进行修改。 这种适应性可以扩展到从 Dataverse 用户表切换到 Office 365 连接器,而无需修改分散在整个应用程序中的公式。

另一种方法是优化 countRows

varListItems = CountRows(SampleList)

使用 Set 函数时,变量 varListItems 必须使用示例列表中的初始行数进行初始化,并在添加或删除列表项后再次设置。 使用命名公式时,当数据更改时,varListitems 变量将自动更新。

App.Formulas 属性中的命名公式为管理整个应用程序中的值和计算提供了一种更具灵活性和声明性的方法,与仅依赖 App.OnStart 相比,这在时间独立性、自动更新、可维护性和不可变定义方面具有优势。

方面 命名公式 (App.Formulas) App.OnStart
时间独立性 公式可立即可用,可以按任何顺序进行计算。 变量可能会引入时间依赖关系,会影响可用性。
自动更新 依赖项更改时,公式将自动更新。 变量在启动过程中设置一次;可能需要手动更新。
可维护性 将公式集中到一个位置,可提高可维护性。 分散的变量可能需要在多个位置查找和更新。
不可变定义 App.Formulas 中的公式定义是不可变的。 变量值可能容易受到意外更改。

用户定义函数

在 Power Apps 编辑器中,用户定义函数(UDF)允许用户创建自定义函数。

要使用此功能,请在预览设置中启用用户定义函数(UDF)。 预览功能不应在生产中使用,这就是默认情况下禁用预览功能的原因,但很快就会正式发布。

如下所示在 App.Formulas 下定义一个公式:

FunctionName(Parameter1:DataType1, Parameter2:DataType2):OutputDataType = Formula

代码的工作方式如下:

  • FunctionName 用于调用函数

  • Parameter 是输入的名称。 允许一个或多个输入

  • DataType 是传入函数的参数,必须与该数据类型匹配。 可用数据类型包括布尔型、颜色型、日期型、日期时间型、动态型、GUID 型、超链接型、文本型和时间型

  • OutputDataType 是函数输出的数据类型

  • Formula 是函数的输出

// Function to calculate the area of a circle based on the radius
calcAreaOfCircle(radius: Number): Number = 
    IfError(Pi() * radius * radius, 0);

使用 IfError 在定义的函数中执行错误处理。

从文本/标签控件调用定义的函数。

calcAreaOfCircle(Int(*TextInput1*.Text))

备注

这是一项试验性功能,可能会发生变化。 某些数据类型(如记录和过滤器)尚不支持。

优化变量

变量定义和设置您在整个应用中使用的局部和全局值。 虽然它们很方便,但使用过多的变量会降低应用的效率。

下面的示例演示了为对象的每个属性设置一个变量,这需要为每个属性使用 Set

Set(varEmpName, Office365Users.MyProfile().DisplayName);
Set(varEmpCity, Office365Users.MyProfile().City);
Set(varEmpPhone, Office365Users.MyProfile().BusinessPhones);
Set(varEmpUPN, Office365Users.MyProfile().UserPrincipalName);
Set(varEmpMgrName, Office365Users.ManagerV2(varEmpUPN).DisplayName);

更有效的方法是仅在需要时才使用该属性:

Set(varEmployee, Office365Users.MyProfile())
"Welcome " & varEmployee.DisplayName

合理使用上下文变量和全局变量。 如果变量的范围超出单个屏幕,请使用全局变量而不是上下文变量。

太多未使用的变量会增加内存使用量,并可能减慢应用初始化速度。 即使您不使用这些变量,也会为它们分配资源。 未使用的变量也会增加应用逻辑的复杂性。 虽然影响可能并不严重,但保持 Power App 整洁有序是一种很好的做法,以获得更好的性能和更轻松的开发。

优化集合

集合是用于在 Power Apps 应用中存储和操作数据的临时数据存储结构。 但是,如果集合使用过多,可能会导致性能开销。 限制集合的使用,仅在必要时使用它们。

// Use this pattern
ClearCollect(colErrors, {Text: gblErrorText, Code: gblErrorCode});

// Do not use this pattern
Clear(colErrors);
Collect(colErrors, {Text: gblErrorText, Code: gblErrorCode});

要计算本地集合中的记录,请使用 CountIf 而不是 Count(Filter())

使用集合时,请考虑以下指南:

限制集合的大小和数量。 由于集合是应用的本地集合,因此它们存储在移动设备内存中。 集合存储的数据越多,或使用的集合越多,性能越差。 使用 ShowColumns 函数仅获取特定列。 添加 Filter 函数仅获取相关数据。

以下示例函数返回整个数据集。

ClearCollect(colDemoAccount, Accounts);

将此内容与以下代码进行比较,后者仅返回特定的记录和列:

ClearCollect(colAcc,
              ShowColumns(
                Filter(Accounts, !IsBlank('Address 1: City')),
                "name","address1_city"))

此示例返回以下数据集:

一个数据集的屏幕截图,其中有一个名为 colAcc 的表和两列,即 address1_city 和 name

设置数据源刷新频率。 如果将新记录添加到集合中,请刷新该集合或收集到集合中以获取新的记录或更改的记录。 如果多个用户更新数据源,请刷新集合以获取新的或已更改的记录。 更多的刷新调用意味着与服务器的交互更多。

缓存集合和变量中的数据

集合是一个表变量,用于存储数据的行和列,而不仅仅是单个数据项。 集合之所以有用,主要有两个原因:在将数据发送到数据源之前聚合数据,以及缓存信息以避免频繁查询。 由于集合与数据源和 Power Apps 的表格结构相匹配,因此即使在离线状态下,您也可以高效地与数据进行交互。

// Clear the contents of EmployeeCollection, it already contains data
ClearCollect(
    colEmployee,
    {
        Id: "1",
        Name: "John",
        Department: "IT"
    },
    {
        Id: "2",
        Name: "Nestor",
        Department: "IT"
    }
)

删除未使用的变量和媒体

虽然未使用的媒体和变量可能不会对应用性能产生重大影响,但请务必通过删除任何未使用的媒体或变量来清理应用。

  • 未使用的媒体文件会增加应用大小,从而减慢应用加载时间。

  • 未使用的变量会增加内存使用量,并可能略微减慢应用初始化速度。 即使未使用这些变量,也会为这些变量分配资源。 太多未使用的变量也会使应用的逻辑更加复杂。

  • 使用应用检查器查看未使用的媒体和变量。

优化屏幕和控件

避免交叉引用控件

引用其他屏幕控件的控件会降低应用程序的加载和导航速度。 这样做可以强制应用立即加载其他屏幕,而不是等到用户转到该屏幕。 要解决这个问题,可以使用变量、集合和导航上下文来跨屏幕共享状态。

Power Apps Studio 中的应用程序检查器会显示相互引用控件。 定期查看应用检查器以解决此问题。

下面是交叉引用控件的示例。 在下图中,库 1 控件在屏幕 2 的标签 2 控件中交叉引用。

显示交叉引用控件的 Power Apps Studio 的屏幕截图

如果在第二个屏幕中引用应用中第一个屏幕中的控件,则不会对性能造成影响,因为第一个屏幕已加载。 这实际上是一件好事,因为该应用程序是声明性的,而不是使用变量。

如果您引用了尚未加载的控件,例如第一个屏幕引用了来自屏幕 3 的名称为 Label 3 的控件,则应用程序会将该屏幕加载到内存中。

为文本控件启用 DelayOutput

当 DelayOutput 设置为 true 时,用户输入将在半秒延迟后注册。 这对于延迟耗时操作直至用户完成文本输入非常有用,例如在其他公式中使用输入时进行过滤。

例如,对于库,其项根据用户在 TextInput 控件中输入的内容进行筛选:

  • 将 DelayOutput 设置为 false(默认值)时,只要键入任何文本,就会筛选库。 如果您的库包含大量项目,则立即重新加载包含更改的库会降低性能。 最好稍等片刻。 当您使用 TextInput 搜索字符串时,这很实用(请参阅搜索或新的 StartsWith 函数)。

  • 将 DelayOutput 设置为 true 时,在检测到更改之前会有短暂的延迟。 这使您有时间完成键入。 延迟适用于 TextInput.OnChange 属性。 如果您有与更改绑定的操作,您不希望它们在您完成字段输入前触发。

委派和服务器端处理

代理

Power Apps 中的委派是一个概念,指的是应用程序将某些操作卸载到底层数据源,而不是在 Power Apps 中处理操作。 通过在 Power Apps 中使用委派,开发人员可以创建更高效、可伸缩的应用程序,即使在涉及大型数据集的场景中也能运行良好。 了解特定数据源和操作的委派限制并据此设计应用程序以实现最佳性能非常重要。

![注释]并非所有函数都是可委派的。 请参考了解委派,了解更多关于委派的信息。

委派具有查询优化等多个优势,并且可添加对大型数据集的支持。 此外,如果源数据频繁更改,则委派可帮助使数据保持最新。

减少对数据源的 API 调用

有时,通过在画布应用中执行联接来创建集合似乎很方便。 下面是一个示例:

在这个例子中,有两个表:驾驶员和卡车。 该代码创建司机和卡车详细信息的集合,对于每辆卡车,它都会呼叫拥有卡车的司机。

// Bad code
ClearCollect(vartruckdata, AddColumns('Truck Details',
    "CITY",LookUp(Drivers, 'Truck Details'\[@'Dummy ID'\] = Drivers\[@'Truck Details'\],City),
        "FIRSTNAME",LookUp(Drivers, 'Truck Details'\[@'Dummy ID'\] = Drivers\[@'Truck Details'\],'Driver First Name'),
    "LASTNAME",LookUp(Drivers, 'Truck Details'\[@'Dummy ID'\] = Drivers\[@'Truck Details'\],'Driver Last Name'),
        "STATE",LookUp(Drivers, 'Truck Details'\[@'Dummy ID'\] = Drivers\[@'Truck Details'\],State)));

在画布应用中执行此类联接可能会生成对数据源的许多调用,从而导致加载时间变慢。

更好的方法是:

// Good code
Set(
    varTruckData,
    LookUp(
        Drivers,
        'Dummy ID' = ThisRecord.'Dummy ID',
        'Driver First Name'
    ) & LookUp(
        Drivers,
        'Dummy ID' = ThisRecord.'Dummy ID',
        'Driver Last Name'
        )
);

Set(
    varTruckData,
    With(
        {
            vDriver: LookUp(
                Drivers,
                'Dummy ID' = ThisRecord.'Dummy ID'
            )
        },
        vDriver.'Driver First Name' & vDriver.'Driver Last Name'
    )
)

在实时场景中,通过在源头修复数据,可以将加载时间从 5 分钟缩短到 10 秒以内。

服务器端处理

不同的数据源,如 SQL 和 Dataverse,允许您将数据处理任务(如筛选和查找)委托给数据源。 在 SQL Server 中,您可以创建由查询定义的视图。 在 Dataverse 中,您可以创建低代码插件在服务器端处理数据,并仅将最终结果返回给您的画布应用。

将数据处理委派给服务器可以提高性能,减少客户端代码,并使应用更易于维护。

了解有关 Dataverse 中插件的更多信息。

优化查询数据模式

使用显式列选择

默认情况下,所有新应用都启用显式列选择 (ECS) 功能。 如果您的应用未启用此功能,请将其打开。 ECS 会自动将检索到的列数减少到仅在应用中使用的列数。 如果 ECS 未开启,您可能会收到比您需要更多的数据,这可能会影响性能。 有时,当应用通过集合获取数据时,列的原始源可能会丢失。 如果 ECS 无法判断列是否已使用,则会删除列。 要强制 ECS 保留缺失的列,请在集合引用后或控件中使用 PowerFx 表达式 ShowColumns

避免调用 Power Automate 以填充集合

一种常见的做法是使用 Power Automate 来获取和填充 Power Apps 中的集合。 虽然这种方法是有效的,但在有些情况下,可能不是最有效的选择。 调用 Power Automate 附带有网络开销,并增加了 0.6 秒的性能成本以实例化 Power Automate 流。

过度使用 Power Automate 流也会导致执行限制和节流。 因此,请始终评估网络延迟和性能成本之间的权衡。

消除 N+1 问题

N+1 问题是数据库查询中的常见问题,指的是在一个查询中没有获取所有必需的数据,而是进行了多个额外的查询来检索相关数据。 这样可能会导致性能问题,因为每个额外查询会产生开销。

类似用于加载集合的简单调用可以生成对数据源的 N+1 次调用。

ClearCollect(MyCollection, OrdersList,
    {
        LookUp(CustomersList,CustomerID = OrdersList[@CustomerID])
    }
)

在画布应用程序和库的上下文中,当使用显示相关记录的数据源和库时,可能会出现 N+1 问题。 当对库中显示的每个项目进行更多的查询时,通常会出现此问题,从而导致性能瓶颈。

使用 SQL Server 中的视图对象来避免 N+1 查询问题,或者更改用户界面来避免触发 N+1 方案。

Dataverse 会自动获取相关表所需的数据,并且可以从相关表中选择列。

ThisItem.Account.'Account Name'

如果 RelatedDataSource` 大小很小(<500 条记录),您可以将其缓存在一个集合中,并使用该集合来驱动 Lookup (N+1) 查询场景。

限制包大小

虽然 Power Apps 对优化应用程序加载很有帮助,但是您可以采取措施减少应用程序的内存占用。 对于使用旧设备的用户,或者在延迟较高或带宽较低的地区的用户来说,减少占用空间尤为重要。

  • 评估您的应用程序中嵌入的媒体。 如果未使用某些内容,请将其删除。

  • 嵌入的图像可能太大。 而不是 PNG 文件,查看是否可以使用 SVG 图像。 但是,使用 SVG 图像中的文本时要小心,因为所使用的字体必须安装在客户端上。 当您需要显示文本时,一种出色的解决方法是使文本标签显示在图像上。

  • 评估分辨率是否适合外形尺寸。 移动应用的分辨率不必与桌面应用程序的分辨率一样高。 尝试正确平衡图像质量与大小。

  • 如果有未使用的屏幕,请删除它们。 注意不要删除任何只有应用程序制作者或管理员使用的隐藏屏幕。

  • 评估您正尝试将过多的工作流放入一个应用程序中。 例如,您是否在同一应用程序中同时具有管理员屏幕和客户端屏幕? 如果是,请考虑将它们分成单独的应用程序。 这种方法还可以使多个用户更轻松地同时处理应用程序,并在应用程序的更改需要全面测试时限制“影响范围”。

优化 ForAll

Power Apps 中的 ForAll 函数用于遍历记录表,并对每条记录应用一个公式或一组公式。 虽然该函数本身用途广泛,但不当使用 ForAll 函数会使您的应用程序性能降低

ForAll 函数是序列函数,而不是并行函数。 因此,它一次只查看一条记录,获得结果,然后继续查看下一条记录,直到遍历完其范围内的所有记录。

尽可能避免 ForAll 的嵌套。 这可能导致指数级迭代,并显著影响性能。

ClearCollect(FollowUpMeetingAttendees.ForAll(ForAll(Distinct(AttendeesList.EmailAddress.Address).Lookup(Attendees))))

数据库的批处理更新

ForAll + Patch 可能是批量更新数据库的一种方法。 但是,在使用 For All 和 Patch 的顺序时要小心。

以下函数:

Patch(SampleFoodSalesData, ForAll(colSampleFoodSales,
    {
        demoName:"fromCanvas2"
    })
);

性能优于:

ForAll(colSampleFoodSales, Patch(SampleFoodSalesData,
    {
        demoName:"test"
    })
);

下一步