Performance considerations with PowerApps
Performance is a top priority for the PowerApps engineering team. There are major efforts currently underway to improve performance around app load time, the designer experience in PowerApps Studio, connectors response time…etc. While the infrastructure will continue to improve, there are a number of things you can do as an app maker to improve the performance of your app. In this blog post I will touch on few concepts that are universal when talking about building native or web apps but apply equally well to apps built with PowerApps.
Loading Data
PowerApps has two types of connectors: standard connectors and custom connectors. There are over 160 standard connectors today and the list is growing. There are two types of standard connector depending on the shape of data returned. The first type returns tabular data. SharePoint, Common Data Services, SQL are examples of tabular data sources where the data returned is in the form of a table. The other type of connector work with function based data sources. Office365, Twitter, Outlook connector are examples of function-based connectors where data is returned with a specific schema. This blog will focus on tabular connectors but the concepts apply as well to the other type of connectors. I will show how some trade offs in the way we load and store data can lead to significant performance improvement and thus improved user experience. For this example, we will use SQL as our data source. We will work with 2 tables: Project & Owners. Our UI is simply a gallery bound to the project table. Each item in the gallery will show the start/end dates, project name and owner. We will explore 2 ways of wiring this simple scenario and compare the resulting experience and performance.
Option 1:
In this example, we will bind our gallery Items property directly to the data source [dbo].[Project]. The project table has a foreign key linking it to the Owner table. So in order to display the actual owner of a project, we will need a LookUp from each of the project into the Owner table as follow:
When the app is loaded, 34 calls were made through the SQL connector and took 6.582 seconds to complete (broadband connection). Taking a closer look at the timeline as seen in a fiddler network trace, we see that a call to fetch the Project table , then each visible gallery item (and few more) triggered a call to the owner database to fetch owner information for that item.
Here is how the loading experience looks like. Notice the blank screen and loading artifacts for the Owner name.
Option 2:
In this example, we will first collect the tables into local collections. We will bind the gallery Items property to the local collection and use the owner collection for LookUps. On the OnVisible event, we add the following statements:
This is the network traces when the app starts.
As you can see only 2 calls are being made. The first call fetches a maximum of 500 items from the Project Table and similiarly the 2nd call will do the same for the owner table. Because the LookUp used in each item is referencing the local in-memory collection, we don’t additional network calls. Overall, the duration of these 2 calls is 3.170 seconds. That is more than 50% faster than the above example. Notice how the data is rendered immediately after the loader animation is done.
Remarks
- ClearCollect only works when delegation is not an issue. By default, the collection created using ClearCollect only holds a maximum of 500 rows. This value can increased but that can impact negatively your performance as you will need to bring a bigger payload. So keep in mind delegation when opting for Option 2.
- From a user experience, I prefer using Option 2. It gives us the opportunity to show a pre-loader experience that itself can be delightful and helps set user expectation. When the data is ready to be loaded, it’s rendered right away which is not the case in Option 1 where we can clearly see the owner is progressively rendering for each items. This tends to make the overall experience jerky as elements are snapping into view without any easing animation that would be easier on the eye. Option 2 will also yield to a much smoother scrolling as data would have already being loaded.
- Option 1 – makes a lot more calls but ends up the total size of the payload downloaded is smaller. In this particular case, only the top 100 items from the project table is loaded and about a dozen of items are loaded from the owner table. On the other hand, Option 2 will load a maximum of 500 items for each of the tables. In this case, 493 items from projects and 32 owners even though they won’t be shown at once.
WARNING: Option 1 should be avoid at all cost as it can lead to N+1 queries that can strain your back-end causing serious degradation of perf.
ConcurrentCall
Concurrent call helps make call to your tables in parallel. You can read all about it in this blog post:
In our example, I am able to wrap my ClearCollect calls into the Concurrent function which result in the trace below.
As you can see. the Calls were made in parallel resulting in a faster overall loading experience. The only limitation to keep in mind is that you won’t be able to use Concurrent function in cases you use the result of one query in the subsequent calls.
Delayed Load
When we bind a table directly to a UI Element as in Option 1, the call to the table will happen automatically when the app loads even if the screen is not yet visible. If you have app with 10 screens and each screen has multiple controls directly bound to your data source, at load time, your app will make a pretty significant number of network calls as it evaluates each one of these controls across all screens. There is an experimental flag that one can set to avoid this behavior:
NOTE: There is a known issue with using Delayed Load flag in conjunction with the Rules feature. Please refrain from enabling this flag if are using Rules in your app.
Patterns
Pattern 1: Load Data From Server
As mentioned before, Option 2 leads to a better user experience as we can now add a pre-loader/splash animation which tends to help set the expectation with the user. The pattern is as follow:
Pattern 2: Load Data From Server with Local caching
The following pattern works great when we want to improve the experience for returning users. When the app loads the first time, it will save the data in the device local storage. The 2nd time the users opens the app, he/she will see data right away. We proceed to refresh the data from the server immediately.
Pattern 3: Concurrent Calls
In the previous pattern, the calls to our data source were made serially. We can further push the performance by initiating concurrent calls. To do that, we make use of the Timer control. To keep things simple, we can create a splash screen where we initiate our calls, then navigate to our main screen when the data is done loading. In this example, I am initiating 2 concurrent calls using Timer Data Source 1 and Timer Data Source 2. You can add more timers if you need to have more calls in parallel but keep in mind that there is a limit of 30 concurrent calls. I also make use of another timer, the Navigate Timer with the only purpose to check if our data has indeed completed loading.
Here is how you would set this up in PowerApps (click on image to enlarge)
Limits
When deploying your app to a large number of user, one has to keep in mind the connectors are throttled. PowerApps currently support 600 requests per minute per user and up to 30 concurrent calls per user.
Custom API
If you are using custom APIs in your apps, you should enable server-side caching as you would do normally three tier applications. If your backend is built on .NET technologies, there are many available libraries that are easy to integrate that would enable server side caching on your APIs. Here are couple of examples https://www.hanselman.com/blog/NuGetPackageOfTheWeekASPNETWebAPICachingWithCacheCowAndCacheOutput.aspx & https://github.com/filipw/Strathweb.CacheOutput.
File Optimization
Another important aspect that affects the performance of your app is file size. Any asset (e.g image, video, audio) that your embed or reference in your app should be optimized to reduce its file size. There are a number of tools that will help you compress your assets so that you reach the optimal file size without compromising quality. Smaller asset size means faster download time. A good approach when dealing with large size images is to produce lower resolution thumbnails of the images to be loaded first and then add another image control on top that will load the actual high resolution images when it’s done downloading. Another useful tip is to pre-load your images in a loading screen by binding to a transparent gallery with image controls bound to the image URLs. The net result of this is to force a network call which caches the images locally. The next time the image is used in your user interface, the image will be fetched from your local cache.
Here are some online tools that can help you optimize your image assets:
https://compressor.io/
https://tinypng.com/
https://compressjpeg.com/
Controls Optimization
Another aspect that can affect performance is the number of screens and controls used in your app. For each UI element, PowerApps will end up generating HTML DOM elements. Minimizing the number of dom elements can help boost your performance. It will improve the performance while authoring the app. There are strategies to optimizing the number of controls used in your app. For example, you can use a gallery control instead of a Canvas/Data Cards when the data displayed is uniform or vary only slightly. Gallery can be powerful in reducing complexity, making your app easier to maintain. Consider the following example:
One obvious approach to achieve the above UI is to layout all of the controls (a total of 27 including right arrows, rectangles, separators and labels). Instead one construct a collection and bind it to a gallery as follow.
As you can see, the number of controls needed is much less. Updating the UI becomes easier as changing the gallery template is reflected in all items. The data itself can be dynamically fetched from an external source like SharePoint or CDS.