App Shell

What is and how to use our Application Shell in any Ionic application.

This feature is only available in PRO version.

An application shell (or app shell) architecture is one way to build a Progressive Web App that reliably and instantly loads on your users' screens, similar to what you see in native apps.

The shell is the minimal CSS, HTML and JavaScript required to power the user interface and when cached offline can ensure instant, reliably good performance to users.

The main goal of the app shell is to improve our app’s perceived performance.

We included the app shell pattern in this ionic 5 template because we believe it is a step forward in terms of user experience. However, if you don't want to use it, you are free to remove it from your app.

Understanding how to use the App Shell can be tricky, that's why we will try to do our best to explain all the concepts related to this topic.

If you feel that the Shell is too complicated for you, and you want a simpler version of the template, on February 2021 we released a new simplified BASIC version.

This new version doesn't have advanced features such as the app shell, server side rendering, and data stores, so it may be more suitable for beginners.

Route Resolves and App Shell

We have already talked about Angular Route Resolves to fetch data while navigating between routes and also we saw the importance of using App Shell components to improve perceived performance. Both are fundamental for our solution, let’s see how to combine them.

In the template we created a Showcase section with examples to demonstrate the difference between the implementations of Route Resolvers. I suggest you the check them in the live preview of the template.

Blocking Resolvers

By default, Angular Route Resolvers won't transition to the page until the Resolver Observable completes.

Let's suppose the backend is slow and takes 5 seconds to fetch data and return it to the client.

Expected behavior: Page transition will be delayed 5 seconds until the server sends data back to the client.

If we would use Route Resolvers out of the box, we could easily show a spinner using the Ionic LoadingController while the app is fetching the data from the backend. Once the data is resolved, the navigation transition to the page and the availability of the data are immediate. This leaves no reason to use skeleton content as we have the data to fill the page content as soon as the page gets activated. However, the user will see a "Loading..." message while waiting for the data to be loaded. Not good for UX!

Non Blocking Resolvers

To avoid waiting for the Observable to complete, we can wrap the base Observable (the one we are getting data from) with a dummy Observable, Subject or Promise that emits the base Observable and immediately completes.

Expected behavior: Page transition will be instant and the transitioned page will show a blank state while the server sends data back to the client.

non-blocking.resolver.ts
resolve() {
  // Base Observable (where we get data from)
  const dataObservable = this.showcaseService.getData();

  // Resolver using a Promise that resolves the base Observable
  const observablePromise = new Promise((resolve, reject) => {
    resolve(dataObservable);
  });
  return observablePromise;
}

With Non-Blocking Resolvers, page transition is immediate. However UX degrades because you are showing a static loading indicator while the server sends data back to the client.

Progressive Shell Resolvers

Now that we found a non-blocking approach to use in our Angular Route Resolvers, we need to find a solution that enable us to resolve a shell model while we wait for the real data from the backend, and once we have the real data, progressively translate the shell model to the real data.

Creating Shell Elements

We created a series of shell elements that will help us build a UI that progressively translate from the shell model loading state to the final state displaying real data.

The idea behind these shell elements is to show a loading state when the element is binded to an empty/null object and then progressively degrade the loading state once the binded object has the real data.

Adding a Shell Provider to the mix

Finally, we need to create some sort of mechanism that allows our stream of data to be cached and pushed. Following Gregg Jensen inspiration we created a solution based in DataStores and DataSources.

DataStore

  • Has a Subject (named timeline) which holds a reference to the state emitted by the DataSource.

  • It has a mechanism to append a Shell (empty model with skeleton components) before the DataSource emits the real data.

  • State management can become a tough task thus why we rely heavily in rxjs to handle the states. If you don't have experience in rxjs I strongly recommend you to read https://www.learnrxjs.io/

DataSource is a simple Observable to get your data.

Expected behavior: Page transition will be instant and the transitioned page will show some shell elements while the server sends data back to the client.

How to use the shell in a page

There are plenty of example in the templates code under the showcase section: src/app/showcase/

Learn how to add the Shell Components to your own Ionic project in the following video.

We built an example ionic tabs demo app with three example of how to use the shell components.

You can get the code of this demo for free. Just contact us and we will give you access to the private repo 🙌🏽.

How to remove it from a page

The Notifications and the Contact Card pages, don't use the App Shell, so if you don't want to use the shell on your app, use this page as a reference.

Also, in the showcase section there are examples of this.

Debugging the Text Shell

Sometimes you will need to debug the shell to adjust the styles and see if everything is working as expected.

We defined an easy and unobtrusive way of setting the shell in debug mode in src/environments/environment.ts

Just set debug to true and you will be able to debug all shells from the app.

src/environments/environment.ts
export const environment = {
  production: false,
  appShellConfig: {
    debug: false,
    networkDelay: 1000
  }
};

We wait on purpose 1 secs (1000 ms) on local environment when fetching data from json files to simulate the backend roundtrip. However, in production environment you should set this delay to 0 in src/environments/environment.prod.ts

Last updated