App.StartScreen: a new declarative alternative to Navigate in App.OnStart
Note: If you are here because you have an app that was created before March 2021, that is now showing an error for using Navigate in OnStart, skip ahead to the section on Older apps with new Navigate workaround. It is an uncommon situation and there is a simple fix.
Everyone loves App.OnStart. It is widely used for the initialization of global variables, the prefetching of data into collections, and to determine which screen should be shown first. It has been widely successful and we promote its use heavily in our performance optimization guidance.
But there’s a problem with this wonderful property. It is imperative. It is an ordered list of work that needs to be done before the first screen is shown. Because it is so specific about not only what needs to be done, but also when that work must be done based on order, it limits the reordering and deferring optimizations that could otherwise be done.
The situation became acute with our Microsoft Teams integration. Having quick app load times is always important, but for Teams it is critical – apps don’t stay in memory, they are expected to load and close within a few seconds at most. Analysis found that App.OnStart was a major culprit for slow loading apps in teams, and since it is imperative, there isn’t very much the system could do to improve the performance.
So, today, we are all embarking on a journey. Over the course of many months we are going to provide new declarative alternatives for all the things you do in App.OnStart today. Not only will your apps load faster and give a better user experience, but it will be easier to write and debug them too. Declarative means that you continue to tell us what you want to happen, but you don’t need to tell us when or how. For example, an Excel spreadsheet describes a set of dependencies between cells (what), it doesn’t say when they should be recalculated or in what order (when and how), that is Excel’s job. Going declarative relieves a lot of the tedium of keeping state variables up to date.
To be clear, we have no plans to eliminate App.OnStart. There will likely always be a few outlier reasons to use it, but we do expect that it will become the exception rather than the rule. That’s a big shift from today. And that won’t happen overnight as we need, together, to determine the best declarative alternatives. Your engagement and feedback is the only way this will work.
We are not forcing you to change anything today. Your apps will continue running App.OnStart as they always have. I’m just unrolling the roadmap out on the table and pointing out our first milestone: App.StartScreen.
By default, the first screen shown when an app starts is the first is in the Tree View in Studio. That behavior can be modified today by calling the Navigate function from within App.OnStart.
This poses a performance problem. If App.OnStart contains a Navigate function call, even if it is in an If function and rarely called, we must complete execution of the App.OnStart before we show the first screen of the app. Ideally we would show the first screen as soon as its dependencies are satisfied, but defer work that are only required by other screens of the app.
App.StartScreen is the new declarative way to indicate which screen should be shown first, that doesn’t block optimizations.
Where you may have written this in the past:
App.OnStart = Collect( OrdersCache, Orders ); If( Param( "AdminMode" ) = "1", Navigate( AdminScreen ), Navigate( HomeScreen ) )
Instead, you can write this with version 3.21101 and later:
App.OnStart = Collect( OrdersCache, Orders ); App.StartScreen = If( Param( "AdminMode" ) = "1", AdminScreen, HomeScreen )
I know, they look very similar, it’s a nuanced difference. Things to notice:
- App.StartScreen is describing a calculation, not work to be done. No side effects, no Navigate calls, nothing imperative. It returns the screen object to show first. It is up to the system to decide when this information is needed and when it is no longer relevant.
- App.StartScreen is separated out from App.OnStart in the second case. They are independent and can be evaluated separately. We can show the first screen before App.OnStart is complete. This is huge. Today, if the App.OnStart contains a Navigate call, we can’t show the first screen until the entire App.OnStart has completed, blocking optimizations that would improve app load time.
Check out the documentation for more details.
If you put your existing knowledge of Navigate in App.OnStart aside, it is actually very easy to explain. To specify the start screen for an app, provide the screen name in the App.StartScreen property:
Screen2 in this case can be replaced by a formula that can take into account the current user, a Param function call, database records, etc.
You can test that this works by selecting Navigate to StartScreen on the … menu for App:
And just like that, we are on Screen2, just as if the app had just started:
Retiring Navigate in App.OnStart
By introducing App.StartScreen, we are also going to begin the process of retiring the use of Navigate in App.OnStart.
Do not be alarmed! Your apps can keep calling Navigate in App.OnStart. Existing apps should see no change in behavior (with one small exception, noted below, and there is a an easy workaround).
There is a new switch in Settings that enables Navigate in App.OnStart. For existing apps, that make use of Navigate in OnStart, this switch will be On. For new apps, this switch will be off by default but can be turned on.
With the switch off, you will see an error in Studio when trying to use Navigate in App.OnStart. With the switch on, the message is only a warning.
Older app with new Navigate workaround
Here’s the exception mentioned before. If you had an existing app created before March 2021, that did not have a Navigate call in App.OnStart, to which you added Navigate to the OnStart between March and now, then the above switch will be turned off and you will see an error the next time you load the app in Studio. Please turn the above switch on to clear the error. We have a fix for this case that will be rolling out soon, but it will be a few weeks until it is fully deployed.
Retired “Enhanced formula bar”
One more caveat. StartScreen is not compatible with the previously experimental and now retired Enhanced formula bar. The new property won’t appear in the list of properties for the App. To use StartScreen, it is time to turn off the enhanced formula bar, this blog post has more details.
Declarative road ahead: Named formulas
App.StartScreen is just our first step. There are two other important new declarative features in the works.
How many of you use Set in App.OnStart to setup global variables? Ok, everyone raised their hand. What if instead of writing:
App.Onstart = Set( ThisUser, LookUp( Users, Email = User().Email ) )
You instead wrote:
ThisUser = LookUp( Users, Email = User().Email )
I know, yet another very nuanced difference. In the second case, we are defining a named formula rather than using the Set function. This is equivalent to defining a name in Excel by using the formula bar or the Name Manager.
So, what’s better about this approach?
- Like every formula, the value is always up to date. It recalculates automatically as dependencies change. The ThisUser record is always correct, even if the app modifies the current user’s entry in the User data source. It need not be updated manually with anther Set call later.
- ThisUser is always available. There is no time before the app starts running in which it is uninitialized. It will never have a Blank or Null value (unless it doesn’t find the user).
- The formula can’t be modified from somewhere else in the app. Between the time the Set is called and it is used, no other formula can modify the value. That’s great because there is a single source of truth for ThisUser, this formula, nothing else can have an impact on it.
- And the performance benefit: Power Fx only needs to evaluate ThisUser when it is used. Work can be deferred, reordered, or even eliminated. If ThisUser isn’t used by the screens the user visits in a session, then it won’t be evaluated at all for that session.
And if you think that’s good, imagine combining named formulas with the equivalent of Excel’s new Lambda function.
Declarative road ahead: Prefetched and cached data
How many of you use Collect in App.OnStart to prefetch and cache data from a data source? Ok, once again everyone raised their hand.
What if instead of writing this today:
App.OnStart = Collect( CachedOrders, Orders ) Gallery1.Items = CachedOrders Form1.Datasource = Orders Button1.OnSelect = SubmitForm( Form1 ); ClearCollect( CachedOrders, Orders )
You wrote this in the future:
Orders.Prefetch = True Gallery1.Items = Orders Form1.Datasource = Orders Button1.OnSelect = SubmitForm( Form1 )
This example is a little less nuanced. The second form is much simpler, more powerful, and is easier to write. Rather than having each maker manually manage their own caches with Collect in OnStart, the system can do the work on their behalf. Declarative directives like “Prefetch this table” and “Cache this table for two hours” are all that are needed to control the caching. Simple knobs rather than many lines of complex imperative code.
- Formulas in the app don’t need to be modified. With or without caching, Gallery1.Items is bound to Orders. We don’t need to change all the formulas to reference an in-memory collection (CacheOrders) instead of the true data source. This means that prefetching and caching can be added or modified later without changing the behavior of the app.
- Going beyond the non-delegation limit. Collect is limited to 2,000 records max. Getting beyond this limit in a collection requires a lot of complex formula writing and so many people don’t bother. And then they may have a bug when they scale their app up. Using the direct data source can incrementally go beyond this limit.
- Pulling only what is initially needed. Likewise, the Gallery control can page in more data as it needs it. Initially, it may only need the first 100 records. Since it all doesn’t need to be there initially as with App.OnStart, the prefetch directive could indicate how many records should be brought in at first and then the rest paged.
- Cache doesn’t need to be manually updated after a change. If the original data source is updated, the cache has to be updated manually. This is cumbersome and error prone, or the entire cache has to be invalidated which is inefficient.
It’s a journey
Thanks for reading. Let it sink in. We have a journey ahead of us that we believe will result in better and easier to write apps. But to get there we have some old patterns to unlearn and some new patterns to establish. That will be hard at time and will take some effort, but we’ll get there.
It starts with a single first step which for us is App.StartScreen and that is all we adding today. Feel free to post your questions and comments below and in the community forum.