Turbocharge your model-driven apps by transitioning away from synchronous requests

Model-driven apps are highly customizable and can pull data from many different sources to satisfy your business needs.  When using custom logic to retrieve data from Dataverse or elsewhere, one important concept comes to mind – performance.   Pro developers should know how to effectively code, build, and run model-driven apps that load quickly when a user opens and navigates in your app while working on daily tasks.  Typically, pro developers use JavaScript to request data using fetch or XMLHttpRequest.

In the XHR case, browsers still support an older model where data is fetched synchronously.  However, this model causes severe performance issues for end users, especially when the network is slow or there are multiple calls that need to be made.  Essentially, the browser freezes up and the end user is frustrated that they cannot click, scroll, or interact with the page.  See our guidance and MDN’s article for more details on the effect (as well as examples).  Transitioning to asynchronous web requests can shave seconds off end users’ form load times depending on the customizations.

Traditionally, the legacy web client has required certain extensions like ribbon rules to return synchronously, meaning developers were forced to use synchronous requests when requesting data from remote sources.  In Unified Interface, we’ve taken steps to ensure asynchronous communication is supported.  For example:

  • Asynchronous ribbon rule evaluation is supported in Unified Interface.
  • Async OnLoad and OnSave is supported in Unified Interface.
  • Many customers needed to check privileges using Xrm.Utility.getGlobalContext().userSettings.securityRoles, looping through the collection to request role names from Dataverse.  We’ve added the roles property to userSettings to avoid the need to request role names.
  • We’re working on asynchronous evaluation for grid item icons.

Today, running the solution checker will alert you of any detected synchronous requests with the web-use-async rule.

Examples

Let’s see an example in action.  As mentioned earlier, synchronous requests are typically used in ribbon rules since the legacy web client only supported this strategy.

// NOTE: Don't do this!
function EnableRule() {
    const request = new XMLHttpRequest();

    // Pass false for the async argument to force synchronous request
    request.open('GET', '/bar/foo', false);
    request.send(null);
    return request.status === 200 && request.responseText === "true";
}

The rule logic asks the server for some data, parses it, and returns a boolean value as its rule evaluation result.  Again, this freezes the browser for the end user until the request returns from the server.

Let’s see how we can convert this rule from synchronous to asynchronous.  By wrapping the request handlers in a Promise and resolving or rejecting when the request is finished, the rule will not block the browser from performing more work.  Of course, in practice, you will probably use helpers to wrap these types of requests so the Promise code does not need to be duplicated.

function EnableRule() {
    const request = new XMLHttpRequest();
    request.open('GET', '/bar/foo');
    return new Promise(function(resolve, reject) {
        request.onload = function (e) {
            if (request.readyState === 4) {
                if (request.status === 200) {
                    resolve(request.responseText === "true");
                } else {
                    reject(request.statusText);
                }
            }
        };
        request.onerror = function (e) {
            reject(request.statusText);
        };
        request.send(null);
    });
}

Using promises and asynchronous patterns, the user is now free to interact with the page while the request is out getting data.  No more freezing of the user interface while the page is loading!

Here’s another example of using the new roles property of userSettings to avoid synchronous requests.

// NOTE: Don't do this!
function userHasRole(roleName) {
    const roleIds = Xrm.Utility.getGlobalContext().userSettings.securityRoles;
    let hasRole = false;
    roleIds.forEach(function(roleId) {
        const request = new XMLHttpRequest();

        // Pass false for the async argument to force synchronous request
        request.open('GET', '/api/data/v9.0/roles(' + roleId + ')', false);
        request.send(null);
        if (request.status === 200) {
            const result = JSON.parse(request.responseText);
            if (result.name === roleName) {
                hasRole = true;
            }
        }
    });

    return hasRole;
}

Using the new roles feature provided by Unified Interface, no network requests need to be made at all:

function userHasRole(roleName) {
    const matchingRoles = Xrm.Utility.getGlobalContext().userSettings.roles.get(function(role) {
        return role.name === roleName;
    });
    return matchingRoles.length > 0;
}

Here’s a profile of the network requests before and after the above change (assuming the user has 10 roles).

Before (~2.3 seconds):

After (~1.2 seconds):

You can see over 1 second (~50%) shaved off the form load, and the content is rendered much faster without the synchronous requests!

We’ve also created a video tutorial to walk through this and more examples of using asynchronous requests.

We hope you will take advantage of these new capabilities in Unified Interface to delight your end users by providing a more responsive experience.  If you find yourself needing to use synchronous requests in any situation, please let us know so we can enhance the product to accommodate.