Free cookie consent management tool by TermsFeed

Interface Performance Best Practices

Introduction

Designing performant interfaces isn't rocket science. But it can be difficult if you don't understand how interface performance works, or the best practices for how to design interfaces that are quick to load and respond to user interactions.

This page compiles some of the best tips and strategies for designing performant interfaces, as well as some of the most important performance principles to get you (and your interfaces) up to speed quickly.

Build Performant Interfaces

Learn more about interface performance in Appian with this course on building performant interfaces.

Interface performance principles

Key terminology

  • Interface evaluation: The process of rendering an interface by running the rules, functions, logic, and variables that make up an interface expression.
  • Expensive computations: A part of an expression that takes a long time to evaluate. For example, a query that takes a long time to run.
  • Conditional logic: Functions or parameters that execute different actions based on whether a specific condition is true or false.
  • Local variables: Variables that are defined using a!localVariables() and are generally only used in the expression in which they are defined. By default, a local variable is only reevaluated if another local variable that it references is changed. You can control this reevaluation behavior using the a!refreshVariable() function.
  • Rule inputs: Variables that are created in an interface object or expression rule object and are generally used to pass data into or out of interfaces and expression rules to other parts of the application.

Interface evaluations

To render an interface, Appian performs an interface evaluation, meaning it runs the rules, functions, and variables that make up an interface expression to determine what to display to the end user.

The following example shows how an interface looks when the expression is initially evaluated. It shows a text field and a button. On the initial evaluation, the value of local!showTextField is true, which means the text field's showWhen parameter is also true.

Certain triggers, like a user entering text into a field, cause a reevaluation. During this reevaluation, only certain parts of the expression are run again. While interface components are usually evaluated with each trigger, local variables are only reevaluated under certain conditions.

In the following example, when a user clicks the HIDE TEXT button, it triggers a saveInto parameter that updates the value of local!showTextField to false. This causes a reevaluation that updates both components on the page:

  • The text field disappears because the value of showWhen is now false.
  • The button label updates to SHOW TEXT because the value of local!showTextField in the if() function is now false.

The following table specifies what will trigger an interface evaluation, as well as which local variables and interface components are evaluated for each type of trigger. For more information on refresh behavior, see Configuring refresh behavior.

Evaluation trigger Which local variables are evaluated? Which interface components are evaluated?
A user first loads an interface. All; Unless they wouldn't evaluate due to conditional logic. All; Unless they wouldn't evaluate due to conditional logic.
A user interacts with a component that uses a saveInto parameter.
  • Local variables that are updated by the saveInto parameter.
  • Local variables that reference a local variable that is updated by the saveInto parameter.
  • Local variables that have refreshAlways set to true.
  • All; Unless they wouldn't evaluate due to conditional logic.
  • By default, the data parameter of records-powered components like read-only grids, charts, and dropdowns only refresh when a variable it references is updated. You can use the refresh parameters of a grid or chart to modify this refresh behavior.
A local variable uses the refreshInterval parameter in a!refreshVariable().
  • Local variables that are refreshed by the refreshInterval parameter.
  • Local variables that reference a local variable that is refreshed by the refreshInterval parameter.
  • Local variables that have refreshAlways set to true.
A user completes a record action in a dialog.
  • Local variables that have refreshAfter set to "RECORD_ACTION" in a!refreshVariable().
  • Local variables that reference a local variable that is refreshed by the refreshAfter parameter.
  • Local variables that have refreshAlways set to true.

Rule inputs behave a little differently than local variables. Generally, they pass a value into or out of the interface and are not updated unless their value is updated in a saveInto parameter.

However, if you use a local variable as the value of your rule input, then the refresh behavior is identical to the local variable.

For example, let's say you have an interface that uses rule!nestedInterface to display another interface. If you use a local variable to pass in a value to the nested interface, then the rule input in the nested interface would use the same refresh behavior as other local variables.

1
2
3
4
5
6
7
a!localVariables(
  local!ruleInputValue,
  rule!nestedInterface(
    /*ruleInput is a rule input in the nestedInterface interface object*/
    ruleInput: local!ruleInputValue
  )
)

Expensive computations

Expensive computations are a part of an expression that can take a long time to evaluate.

For example:

  • Transforming, organizing, or modifying large amounts of data.
  • Looping over large amounts of data.
  • Querying a lot of data.
  • Performing integration calls.

These types of computations are common and necessary when designing feature-rich applications. You don't need to avoid them. This page outlines some ways you can ensure your interfaces remain performant even when using computations that can take awhile to run.

On the other hand, most functions are super quick to evaluate, such as logical functions like if() and computational functions like sum().

If you're not sure if something takes a long time to compute, check out the performance view for your interface. It tells you how many milliseconds a function, rule, record type query, or local variable takes to compute.

For more information on using the performance view, check out this video from our Appian Community YouTube channel about interface performance tuning.

Conditional logic

Conditional logic refers to functions and parameters that execute different actions based on whether a condition is true or false. They basically say, "If this condition is met, then do this; otherwise, do something else."

When you use conditional logic in an expression, only the necessary conditions are evaluated. Conditions that are not evaluated do not add to the time that it takes for an interface to evaluate.

See When using and(), or() and match() functions, put expensive computations last for information on how you can use this to your advantage in your interface design.

The following is a summary of what is evaluated in conditional functions and parameters.

Conditional Logic What is Evaluated Example
showWhen parameter in a component When the value of a showWhen parameter returns false, the entire component does not evaluate, including any components nested inside of it.

In the following expression, only the Integer section and the two integer fields are evaluated. The Text section and the two text fields are ignored since showWhen is false.

{
  a!sectionLayout(
    label: "Text",
    showWhen: false,
    contents: {
      a!textField(label: "Text 1"),
      a!textField(label: "Text 2")
    }
  ),
  a!sectionLayout(
    label: "Integers",
    showWhen: true,
    contents: {
      a!integerField(label: "Integer 1"),
      a!integerField(label: "Integer 2")
    }
  )
}
if() function If the condition parameter returns true, the first value is evaluated. If it returns false, the second value is evaluated.

In the following expression, only "value1" is evaluated.

if(
  true, /* condition */
  "value1", /* value if true */
  "value2" /* value if false */
)
a!match() function The evaluation stops after a match is found. All values after this are not evaluated. The default value is only evaluated if no match is found.

In the following expression, the evaluation stops at then: "Match 2".

a!match(
  value: 2,
  equals: 1,
  then: "Match 1",
  equals: 2,
  then: "Match 2",
  equals: 3,
  then: "Match 3",
  default: "Default"
)
and() function The evaluation stops after the first value that returns false. All values after this are not evaluated.

In the following expression, the evaluation stops at 1+3=5 since this is false.

and(
  1+1=2,
  1+2=3,
  1+3=5,
  1+4=5
)
or() function The evaluation stops after the first value that returns true. All values after this are not evaluated.

In the following expression, the evaluation stops at 1+2=3 since this is true.

or(
  1+1=3,
  1+2=3,
  1+3=4,
  1+4=5
)
choose() function Only the value whose index matches the index in the first parameter.

In the following expression, only "choice2" is evaluated.

choose(
  2, /* index of the value to return */
  "choice1", /* index 1 */
  "choice2", /* index 2 */
  "choice3" /* index 3 */
)

Local variable best practices

Consider the following best practices when using local variables:

Put expensive computations in local variables, not component parameters

Guideline

Put functions and rules that might take longer to compute, also known as expensive computations, in local variables. Do not put them directly in a parameter of an interface component.

You don't have to worry about this when using the data parameter of records-powered components like read-only grids, charts, and dropdowns. The data parameter of these components has the same default refresh behavior as local variables. On read-only grids and charts, you can modify this default behavior using the refresh parameters the same way you would use a!refreshVariables() for local variables.

Importance

Any time an evaluation is triggered, such as when a user enters a value or clicks a button, all interface components in the interface are reevaluated. This means that anything you put in a parameter of a component will reevaluate every time an evaluation is triggered.

When you put a rule or function call in a local variable, by default it is only evaluated when:

  • The interface is first loaded.
  • The local variable is updated in a saveInto parameter.
  • Another local variable that is referenced in the local variable is updated.

See Local Variables to learn about controlling how often a local variable is refreshed.

For expensive queries that rely on each other, set them up to evaluate in parallel

Guideline

If you have a query in a local variable that references a query in another local variable, consider if it's possible to rewrite the expression so that the variables don't reference each other. This allows them to evaluate at the same time (in parallel), instead of evaluating one after the other (in serial), reducing the overall time it takes to evaluate.

Importance

When a local variable references another local variable, they are evaluated one after another, or "in serial." Appian needs the value of the referenced variable in order to update the parent variable.

However, when local variables are not dependent on each other, the variables are evaluated at the same time, or "in parallel." Because the variables are evaluated at the same time, it takes less overall time to evaluate variables in parallel than it does to evaluate them one after the other.

Parallel evaluation vs serial evaluation

See Parallel Evaluations of Expressions for more information.

Interface design best practices

Consider the following best practices when building your interfaces:

Don't add a lot of user interactions to complex interfaces that display a lot of data

Guideline

Interfaces that display a lot of data, such as reporting dashboards, tend to rely on quite a few queries, which can take longer to evaluate. When you design these interfaces, avoid adding too many user interactions to reduce the number of times the interface is evaluated. Examples of user interactions include changing filter values or entering data into a field.

For more helpful tips about designing these types of interfaces, see the Worker Home Pages design guidance in the SAIL Design System.

Importance

Every time a user interacts with an interface, the entire interface is reevaluated. If a complex interface has a lot of user interactions that trigger an evaluation, the user may have to wait for the information to load each time, which isn't a great user experience.

Use record action components to update data

Guideline

To allow users add, update, or delete data in a complex interface, use a record action component. Do not use components like an editable grid or text input field to allow users to update data directly in complex interfaces.

Importance

While you can allow users to update data directly in an interface using different input components, this can slow performance on complex interfaces that display a lot of data

Instead, you should use a record action component so users can interact with a separate interface to add or update data. By using a record action component, you avoid introducing too many user interactions in your complex interface, and you're reusing record actions configured on your record types, so it's overall faster to configure.

When you have multiple record actions on a complex interface, display them in a menu style

Guideline

If you have multiple record actions on a single page, you should set the record action component style to "MENU" or "MENU_ICON". These styles use the securityOnDemand parameter to determine when record action security is evaluated. By default, this value is set to true.

Importance

When you display a record action on an interface, Appian will evaluate the record action security to determine who can see the action and when. If you have multiple record actions, each action's security will need to be evaluated before a user can interact with the interface. This can impact how quickly the overall interface loads, especially if you display the actions on each row of a grid.

To performantly display multiple record actions in an interface, you should display the actions in a "MENU" or "MENU_ICON" style. These styles not only make the page appear less cluttered, they use the securityOnDemand parameter to determine when record action security is evaluated. By default, this value is set to true, so security is only evaluated when a user clicks an action. Deferring this check until it's needed means the interface will ultimately load faster.

When using and(), or(), and match() functions, put expensive computations last

Guideline

When using and(), or(), and a!match() functions, put more expensive computations later in the expression.

Importance

These functions evaluate their values in order. The evaluation stops when:

  • and(): A value returns false.
  • or(): A value returns true.
  • a!match(): A match is found. After it finds a match, the then parameter is evaluated and the rest of the parameters are ignored.

Any parameters that come after the evaluation stops are not evaluated and don't add to the interface evaluation time. If you put more expensive computations first, these will be needlessly evaluated. Putting them later in the expression ensures that they are only evaluated when needed.

See the Conditional logic interface performance principle for more information.

Don't wrap text in a!richTextItem() if you don't need to style it

Guideline

If you are displaying a text string that doesn't need to be styled, don't put it in a rich text display component. Even when mixing rich and plain text, it is better to provide the plain text as a string, instead of putting in a rich text display component and not styling it.

Importance

Plain text takes almost no time to evaluate. Using plain text wherever possible is a best practice to make sure you aren't adding unnecessary loading time that can be avoided.

Query best practices

Consider the following best practice when querying data in an interface.

Only query the data you need

Guideline

When querying data to display in an interface, only query the exact fields and data you need. Do not return all fields and data from a data entity unless you need to display all of that data in a comprehensive interface.

Importance

The more data you query, the longer it takes to load and display that data on the interface.

Before creating a query, consider what data you actually need to display in the interface—there's no need to query data that you're not going to use.

Then, use functions like a!queryRecordType(), a!queryRecordByIdentifier(), or a!queryEntity() to specify the exact fields to return and apply filters to limit the results to the most relevant data.

Example

Let's say you want to create an interface that highlights your top five customers. For each customer, you want to create a card that shows the customer's name, their industry, and how long they've been a customer.

Do query the fields you need and the exact amount of data you need.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a!queryRecordType(
  recordType: recordType!Customer,
  /* Only query the 3 fields you need to display in the card */
  fields: {
    recordType!Customer.fields.displayName,
    recordType!Customer.fields.createdOn,
    recordType!Customer.relationship.industry.fields.name
  },
  pagingInfo: a!pagingInfo(
    startIndex: 1,
    /* Only return 5 results, and sort those results by customers with the largest year-to-date sales */
    batchsize: 5,
    sort: a!sortInfo(
      field: recordType!Customer.fields.YTDSales,
    )
  )
).data

Don't query all fields in the record type and the related record type. While you'll still get the data you need to create your interface, you're also querying unnecessary data that can impact how quickly the data is returned.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a!queryRecordType(
  recordType: recordType!Customer,
  /* Queries all fields from the Customer and Industry record types */
  fields: {
    recordType!Customer.relationship.industry
  }
  pagingInfo: a!pagingInfo(
    startIndex: 1,
    batchsize: 5,
    sort: a!sortInfo(
      field: recordType!Customer.fields.YTDSales,
    )
  )
)

Feedback