[object Object]

Form load time is the single most visible performance metric in Dynamics 365 because every user feels it on every record open. The Unified Interface ships fast; custom JavaScript is usually what ruins it.

Measure first

Open the form with browser dev tools, Performance tab, record. Look at the Long Tasks ribbon. Each form script handler shows up as a function execution. Anything over 50 ms is a noticeable janky frame; anything over 200 ms is a perceived delay.

Also check the Network tab. Synchronous Web API calls in OnLoad block paint until they return.

Kill synchronous Web API calls

The most common offender. Code like this in OnLoad:

var result = Xrm.WebApi.retrieveRecord("account", id); // returns a Promise
processData(result); // runs before promise resolves -> bug

Then someone “fixes” it by switching to deprecated synchronous XHR, which blocks the UI thread for the full network round trip. On a slow connection, you have just added 800 ms of frozen form.

The right pattern is async with proper UI handling:

async function onLoad(executionContext) {
    const formContext = executionContext.getFormContext();
    formContext.ui.setFormNotification("Loading data...", "INFO", "loadingNotif");
    try {
        const result = await Xrm.WebApi.retrieveRecord("account", id, "?$select=name");
        formContext.getAttribute("name").setValue(result.name);
    } finally {
        formContext.ui.clearFormNotification("loadingNotif");
    }
}

The form paints, the user sees a notification, and data fills in when ready.

Bundle and minify web resources

Dynamics 365 loads each web resource as a separate request. Twenty form scripts means twenty HTTP round trips. Bundle them into one file per app using webpack or esbuild. Add a build step that produces contoso_formscripts.bundle.min.js. Reference the bundle from each form’s libraries section.

A single 200 KB bundle outperforms twenty 10 KB files because of HTTP overhead and parser warmup.

Lazy-load heavy logic

A form might need a complex validator only if a specific field has a value. Do not load the validator unconditionally. Use dynamic import in your bundle:

formContext.getAttribute("contractType").addOnChange(async (ctx) => {
    if (ctx.getFormContext().getAttribute("contractType").getValue() === "premium") {
        const { validatePremium } = await import("./premiumValidator.js");
        validatePremium(ctx);
    }
});

The validator code never downloads for users who never select premium.

Trim OnChange chatter

Every OnChange handler runs for every keystroke if the field is text. Debounce text inputs with a 250 ms timer before doing anything expensive. For numeric and option set fields, OnChange fires once per commit, so debouncing is unnecessary.

Avoid retrieveMultiple in form handlers

If you need related data, prefer a Quick View Form (resolved server-side, single payload) over a JavaScript retrieveMultipleRecords call. Quick Views are cached and rendered with the form; scripts add a round trip.

When you must query, use $select to grab only the columns you need, never $select=*. Add $top=10 even when you expect one row, as a safety net.

Profile in production-like environments

Dev environments are often empty and fast. The form that loads in 1 second on a sandbox with 100 records can take 6 seconds on a production environment with 5 million. Always test against a production data clone before declaring victory.

Set a budget

Adopt a per-form budget: OnLoad must complete (including all async work that paints) within 1500 ms at p95. Build a dashboard from telemetry to track every form against the budget. Forms that miss go on the next sprint.

What to do this week: pick your three highest-traffic forms, measure OnLoad time, and convert any synchronous Web API calls to async with notifications.

[object Object]
Share