在模型驱动应用中设计窗体以提高性能

构建可以快速有效地完成任务的体验对于提高用户满意度至关重要。 模型驱动应用可以高度自定义来创建满足用户需求的体验,但重要的是要知道如何有效地编码、构建和运行模型驱动应用,让这些应用在用户处理日常任务过程中打开和导航您的应用时可以快速加载。 事实已证明,当应用未针对性能优化时,性能是导致用户对应用不满意的关键驱动因素。

巧妙的自定义和高性能的窗体是构建高效、多产的窗体的重要方面。 确保您使用用户界面设计和布局方面的最佳实践构建高效的窗体也很重要。 有关设计窗体以提高效率和生产力的信息,请参阅在模型驱动应用中设计高效的主窗体

确保用户使用推荐的支持设备以及达到最低规范要求也很重要。 详细信息:支持的 Web 浏览器和移动设备

使用数据和选项卡

本节介绍显示数据和选项卡的控件如何影响窗体性能。

默认选项卡的重要性

默认选项卡是窗体上第一个展开的选项卡。 它在加载窗体页面中起着特殊的作用。 按照设计,默认选项卡的控件会始终在打开记录时呈现。 具体来说,将为选项卡上的每个控件调用控件初始化逻辑,如数据检索。

相比之下,二级选项卡在窗体最初加载时不会对其控件执行此初始化。 而是在通过用户交互或调用 setFocus 客户端 API 方法打开二级选项卡时执行控件初始化。 这将提供一个机会,通过将某些控件放在二级选项卡而不是默认选项卡来保护初始窗体加载免于被控件过度处理。因此,控件放置策略可能会对初始窗体加载的响应性产生显著影响。 响应性更好的默认选项卡为修改重要字段、与命令栏交互以及探索其他选项卡和部分提供了更好的整体体验。

始终将最常用的控件放在默认选项卡的顶部。当用户与窗体上的数据交互时,布局和信息体系结构不仅对性能很重要,对提高工作效率也很重要。 详细信息:在模型驱动应用中设计高效的主窗体

数据驱动控件

需要超出主记录的额外数据的控件会对窗体响应和加载速度产生最大的压力。 这些控件通过网络提取数据,通常会有等待期(显示为进度指示器),因为传输数据可能需要时间。

一些数据驱动控件包括:

在默认选项卡上仅保留这些控件中最常用的控件。其余的数据驱动控件应分布到二级选项卡中,以允许快速加载默认选项卡。 此外,此布局策略将减少提取最终未使用的数据的机会。

还有其他一些控件比数据驱动控件影响小,但仍然可以参与上述布局策略以实现最佳性能。 这些控件包括:

Web 浏览器。

本节介绍使用 Web 浏览器时的一些好的做法。

不要打开新窗口

openForm 客户端 API 方法允许参数选项在新窗口中显示窗体。 请勿使用此参数,或将它设置为 false。 将它设置为 false 将确保 openForm 方法执行使用现有窗口显示窗体的默认行为。 也可以从自定义脚本或其他应用程序直接调用 window.open JavaScript 函数;但这也应该避免。 打开新窗口意味着需要从头提取和加载所有页面资源,因为页面无法在先前加载的窗体和新窗口中的窗体之间利用内存中数据缓存功能。 作为打开新窗口的替代方法,可考虑使用允许在多个选项卡中打开记录的多会话体验,同时仍然最大限度地提高客户端缓存的性能优势。

使用现代浏览器

使用最新的 Web 浏览器是确保模型驱动应用尽可能快地运行的关键。 这样做的原因是很多性能改进只能在较新的现代浏览器中使用。

例如,如果您的组织有 Firefox 的旧版本、非基于 Chromium 的浏览器等,模型驱动应用中内置的很多性能提升在旧浏览器版本中将不可用,因为它们不支持应用快速流畅地运行所依赖的功能。

在大多数情况下,您只需切换到 Microsoft Edge,从旧版本更新到最新的当前浏览器版本,或移动到基于 Chromium 的现代浏览器,就会看到页面加载的改进。

JavaScript 自定义

本节介绍如何在使用 JavaScript 时进行合适的自定义,来帮助您在模型驱动应用中构建高性能窗体和页面。

将 JavaScript 用于窗体

JavaScript 自定义窗体的能力在窗体外观和行为方式方面为专业开发人员提供了极大的灵活性。 这种灵活性的不当使用可能会对窗体性能产生负面影响。 开发人员在实现 JavaScript 自定义时应使用以下策略来最大化窗体性能。

在请求数据时使用异步网络请求

当自定义需要额外数据时,异步请求数据而不是同步请求。 对于支持等待异步代码的事件,如窗体 OnLoad 和窗体 OnSave 事件,事件处理程序应该返回 Promise,让平台等待 Promise 结束。 当用户等待事件完成时,平台将显示适当的 UI。

对于不支持等待异步代码的事件,如窗体 OnChange 事件,您可以在代码使用 showProgressIndicator 执行异步请求时使用一种变通方法来停止与窗体的交互。 这比使用同步请求要好,因为当进度指示器显示时,用户仍然可以与应用程序的其他部分进行交互。

这是在同步扩展点中使用异步代码的示例。

//Only do this if an extension point does not yet support asynchronous code
try {
    await Xrm.WebApi.retrieveRecord("settings_entity", "7333e80e-9b0f-49b5-92c8-9b48d621c37c");
    //do other logic with data here
} catch (error) {
    //do other logic with error here
} finally {
    Xrm.Utility.closeProgressIndicator();
}

// Or using .then/.finally
Xrm.Utility.showProgressIndicator("Checking settings...");
Xrm.WebApi.retrieveRecord("settings_entity", "7333e80e-9b0f-49b5-92c8-9b48d621c37c")
    .then(
        (data) => {
            //do other logic with data here
        },
        (error) => {
            //do other logic with error here
        }
    )
    .finally(Xrm.Utility.closeProgressIndicator);

在不支持等待异步代码的事件处理程序中使用异步代码时应该小心。 对于需要对异步代码的解析采取或处理操作的代码尤其如此。 如果解析处理程序预期应用程序上下文保持与异步代码启动时的相同状态,异步代码可能会导致问题。 您的代码应该在每个异步延续点之后检查用户是否处于相同的上下文中。

例如,事件处理程序中可能有代码来发起网络请求,并根据响应数据更改要禁用的控件。 在收到来自请求的响应之前,用户可能已经与控件交互或导航到不同页面。 由于用户在不同的页面上,窗体上下文可能不可用,这可能会导致错误,或可能有其他非预期行为。

窗体 OnLoad 和窗体 OnSave 事件中的异步支持

窗体 OnLoadOnSave 事件支持返回承诺的处理程序。 这些事件将等待处理程序返回任何承诺来解析,直到到达超时期限。 可通过应用程序设置启用此支持。

详细信息:

限制窗体加载期间请求的数据量

仅请求在窗体上执行业务逻辑所需的最少量数据。 尽可能缓存请求数量的数据,尤其是不经常变化或不需要刷新的数据。 例如,假设有一个窗体从设置表请求数据。 根据设置表中的数据,此窗体可能会选择隐藏窗体的一个部分。 在这种情况下,JavaScript 可以将数据缓存在 sessionStorage 中,以让每个会话 (onLoad1) 只请求一次数据。 当 JavaScript 使用 sessionStorage 中的数据同时为下一次导航到窗体 (onLoad2) 请求数据时,也可以使用“重新验证时过期”策略。 最后,如果连续多次调用处理程序 (onLoad3),则可以使用重复数据删除策略。

const SETTING_ENTITY_NAME = "settings_entity";
const SETTING_FIELD_NAME = "settingField1";
const SETTING_VALUE_SESSION_STORAGE_KEY = `${SETTING_ENTITY_NAME}_${SETTING_FIELD_NAME}`;

// Retrieve setting value once per session
async function onLoad1(executionContext) {
    let settingValue = sessionStorage.getItem(SETTING_VALUE_SESSION_STORAGE_KEY);

    // Ensure there is a stored setting value to use
    if (settingValue === null || settingValue === undefined) {
        settingValue = await requestSettingValue();
    }

    // Do logic with setting value here
}

// Retrieve setting value with stale-while-revalidate strategy
async function onLoad2(executionContext) {
    let settingValue = sessionStorage.getItem(SETTING_VALUE_SESSION_STORAGE_KEY);

    // Revalidate, but only await if session storage value is not present
    const requestPromise = requestSettingValue();

    // Ensure there is a stored setting value to use the first time in a session
    if (settingValue === null || settingValue === undefined) {
        settingValue = await requestPromise;
    }
    
    // Do logic with setting value here
}

// Retrieve setting value with stale-while-revalidate and deduplication strategy
let requestPromise;
async function onLoad3(executionContext) {
    let settingValue = sessionStorage.getItem(SETTING_VALUE_SESSION_STORAGE_KEY);

    // Request setting value again but don't wait on it
    // In case this handler fires twice, don’t make the same request again if it is already in flight
    // Additional logic can be added so that this is done less than once per page
    if (!requestPromise) {
        requestPromise = requestSettingValue().finally(() => {
            requestPromise = undefined;
        });
    }

    // Ensure there is a stored setting value to use the first time in a session
    if (settingValue === null || settingValue === undefined) {
        settingValue = await requestPromise;
    }
    
    // Do logic with setting value here
}

async function requestSettingValue() {
    try {
        const data = await Xrm.WebApi.retrieveRecord(
            SETTING_ENTITY_NAME,
            "7333e80e-9b0f-49b5-92c8-9b48d621c37c",
            `?$select=${SETTING_FIELD_NAME}`);
        try {
            sessionStorage.setItem(SETTING_VALUE_SESSION_STORAGE_KEY, data[SETTING_FIELD_NAME]);
        } catch (error) {
            // Handle sessionStorage error
        } finally {
            return data[SETTING_FIELD_NAME];
        }
    } catch (error) {
        // Handle retrieveRecord error   
    }
}

使用客户端 API 中可用的信息,而不是发起请求。 例如,您可以使用 getGlobalContext.userSettings.roles,而不是在窗体加载时请求用户的安全角色。

仅在需要时加载代码

根据需要为特定窗体的事件加载任意数量的代码。 如果您有代码仅用于窗体 A窗体 B,则不应将其包含在为窗体 C 加载的库中。而应该在它自己的库中。

如果代码仅用于 OnChangeOnSave 事件,应避免在 OnLoad 事件中加载库。 而是在这些事件中加载它们。 这样,平台可以将它们的加载推迟到窗体加载之后。 详细信息:优化窗体性能

删除生产代码中使用的控制台 API

请勿在生产代码中使用控制台 API 方法,如 console.log。 将数据记录到控制台会显著增加内存需求,并且可能会在内存中阻止清理数据。 这可能会导致应用随着时间的推移变慢并最终崩溃。

避免内存泄漏

随着时间的推移,代码中的内存泄漏会导致性能下降,并最终导致应用崩溃。 当应用程序不再需要内存但却无法释放时,就会发生内存泄漏。 使用窗体上的所有自定义和代码组件,您应该:

  • 彻底考虑和测试任何负责清理内存的方案,例如负责管理对象生命周期的类。
  • 清除所有事件侦听器和订阅,特别是当事件侦听器和订阅位于 window 对象上时。
  • 清除所有计时器,如 setInterval
  • 避免、限制和清理对全局或静态对象的引用。

对于自定义控件组件,可以在销毁方法中完成清理。

有关解决内存问题的详细信息,请转到此 Edge 开发人员文档

可用于帮助提高应用性能的工具

本节介绍可帮助您了解性能问题,并提供如何优化模型驱动应用中自定义的建议的工具。

性能见解

性能见解是一个面向企业应用制作者的自助服务工具,可分析运行时遥测数据并提供优先推荐列表,来帮助提高模型驱动应用的性能。 此功能提供一组与 Power Apps 模型驱动或客户参与应用(例如 Dynamics 365 Sales 或 Dynamics 365 Service)性能相关的每日分析见解,以及建议和可操作项。 企业应用制作者可以在 Power Apps 中查看应用级别的详细性能见解。 详细信息:什么是性能见解?(预览)

解决方案检查器

解决方案检查器是一个强大的工具,可以分析客户端和服务器自定义的性能或可靠性问题。 它可以解析客户端 JavaScript、窗体 XML 和 .NET 服务器端插件,并针对可能降低最终用户速度的因素提供有针对性的见解。 我们建议您每次在开发环境中发布更改时都运行解决方案检查器,以在到达最终用户之前发现所有性能问题。 详细信息:在 Power Apps 中使用解决方案检查器验证模型驱动应用程序

使用解决方案检查器发现的与性能相关的问题的一些示例:

对象检查器

对象检查器对解决方案中的组件对象运行实时诊断。 如果检测到问题,会返回一条说明如何解决该问题的建议。 详细信息:使用对象检查器诊断解决方案组件(预览)

后续步骤

在模型驱动应用中设计高效的主窗体