Improve App Performance

As your app grows in size and complexity, it may run into performance bottlenecks. This document aims to address common app performance issues and gives you tools to identify performance bottlenecks and to improve the performance of your app.

Advanced Topic

Debugging performance issues requires a systematic approach, and a deeper understanding of how an app is run on specific hardware. This doc assumes that you understand the high-level constraints on app performance i.e. CPU, memory use and graphical rendering.

Typical sources of poor app performance

To start, it is useful to know what the common causes are of poor app performance:

  • The device running the app is slow. If only some users are affected, and they are running older hardware, check to see whether their devices meet the recommended hardware specifications.

  • The app is making queries against big data sets without app indexes.

  • The app is making many queries in sequence (or in parallel).

  • On each digest cycle, the app is performing many operations to refresh the view.

  • The app is rendering a lot of data at once.

This is a non-exhaustive list, but covers the most typical causes.

Each of the above root causes for poor app performance can be pinpointed using the tools described below.

Use the developer console to pinpoint specific issues

Cannot reproduce the issue?

The first step to debugging any performance-related issue is to try and reproduce the issue. If you cannot reproduce the issue it makes it nearly impossible to debug the problem. First spend time and gather information to reproduce the issue before proceeding.

The most effective tool for you to help identify what is causing an app to be slow is the developer debug console. The developer console logs performance-related information.

For a desktop container, confirm that you have a container where the developer tools are enabled. The JourneyApps branded container has developer tools enabled by default. Right-click anywhere in the app and select "Toggle DevTools".

To open the developer console, open the "Console" tab. Be sure to view the Verbose log levels, and for best results, update the developer console settings to include time stamps.

It's also possible to use the developer tools in Web, though note that DB performance could be very different on Web vs other platforms.

We recommend using the developer debug console with runtime version 4.85.0 or greater and enabling journey.profiling, which includes the following performance profiling improvements:

  • View function names are now listed instead of โ€œwidgets-navigateโ€

  • Each digest cycle is being logged

  • More sync system operations are being logged, including expensive DB operations

  • Queries now show indexes where applicable

Also see: Debug console

Common app performance improvements

This section covers the most common ways to improve an app's performance.

Query optimizations

Most performance issues are created by issues related to slow or high volumes of DB queries. For the purpose of this doc, queries refer to both DB and OnlineDB queries, and even belongs-to relationship look-ups.

App indexes

When specific interactions in an app have become reproducibly slow as the data volumes have grown, it might point to a query that requires an app index. App indexes might be required on belongs-to relationships, or where the query result is a small subset of the queried data.

In the developer console, look for single queries that are taking longer than 100ms, especially if the result set is less than 30 objects.

To see whether a query is correctly using an index, or might need one, you can add .explain() to the query and place it in a console.log() during development.

For apps with runtime version 4.85.0 or greater, indexes can be seen together with queries logged in the developer console with journey.profiling enabled.

  • Example query log without indexes: DB.job.where("customer_id = ?", view.customer_id) (type: full scan, results: 1, scanned: 17987)

  • Example query log with indexes: DB.job.where("customer_id = ?", view.customer_id) (type: index, results: 1, scanned: 25 ...)

Queries and loops

As a rule of thumb, avoid making queries in a loop. Queries being run in a loop (for or forEach() etc.) are prone to poor performance, especially on devices with less computing power.

It's often easy to identify queries in a loop by inspecting the code where the app is slow. If you're looking at the developer console, queries in a loop are very easy to spot. It presents as a series of similar queries in very quick succession.

Changing many DB objects in quick succession

Sometimes it is necessary to perform a large number of DB operations in quick succession. If you do this as multiple .save() operations, the DB performance may suffer. A better pattern is to batch them together using Batch Operations.

OnlineDB queries

DB queries are typically very fast, since the application is querying data on a local database. By contrast, OnlineDB (similar to fetch requests) have significant delay to complete because the request - response loop depends on a connection between the app device and remote server.

For that reason, using OnlineDB may exacerbate other sources of poor performance, e.g. querying in a loop, or doing queries in $: functions.

.count() vs .length

.count() is considered a query, and can contribute to poor app performance. If a function may do multiple .count() queries, consider using Arrays instead and use .length, which will execute much faster.

Queries in the view

Queries defined in the View XML are refreshed on every digest cycle. By contrast, arrays are not refreshed on every digest cycle. If a view needs to show data from many queries, but the data sets are not expected to change, try to use arrays instead of queries for view variables.

$: functions and the digest cycle

$: functions are very commonly used to make an app more dynamic and respond to user feedback. What's important to understand about $: functions is that they are run on every digest cycle (more about this below). These functions can be used in the View XML, and in global UI components such as the Navigation component.

The app can take a performance hit when using $: functions, especially when queries are run as part of the functions.

The reason why an app slows down where queries are run in $: functions is linked to the concept of the digest cycle. The digest cycle is the process where the app refreshes what is displayed. A digest is triggered on every saved change on a view, every on-change, every on-press.

For that reason, when $: functions include many queries, or a few slow queries, it means that those queries need to be run every single time the app has to refresh the values displayed. That will mean that users will perceive the app to be slow to interact with.

Views with an editable object-table

Views with large or complicated object-table UI components are specifically prone to poor performance given that it is easy to make many of the above performance mistakes.

Here are some specific performance considerations for object-table:

  • A table often displays large amounts of data, which implies that there is a big (and often slow) query made. Sometimes there are ways to index the query to improve performance.

  • Tables might display related belongs-to data in the table as one or more of the columns. This creates a situation where a belongs-to look-up is being made for each row in the table (effectively a query within a loop). To rectify this, add .include() on the query that is bound to the table for the related object you'd like to display.

  • Editable tables have on-change functions that trigger on each change made in each cell. And in the function lookups might be made or queries may inadvertently be run. App users often want to edit cells in quick succession, which potentially sets off a number of on-change functions, compounding the problem.

  • Tables often use $: functions to update formatting in tables. These functions might contain operations or queries that aren't performant, adding to the digest cycle.

Global UI components

Global UI components (e.g. the navigation drawer) are displayed on most, if not all app views. For some use cases it might be necessary to show the number of objects (e.g. unread messages, or "todo" tasks). Be mindful, however, that any $: functions that do .count() or even other queries, will be run on every digest cycle, and may contribute to an app feeling unresponsive overall.

iOS devices and memory use

Different devices handle high memory and CPU use differently. On iOS an app that is using high amounts of memory may cause a "white screen" where the app is no longer accessible and will need to be restarted. Use the guide above to identify performance bottlenecks and address them.

Last updated