Ionic 5 is a step forward in terms of performance, development experience and Progressive Web Apps support. It's clear that the Ionic Framework team is doing a great job positioning themselves as the framework of choice to build modern mobile and progressive web apps.

In this ionic tutorial we will discover all the possibilities the new Ionic Navigation brings and also talk about some usability tricks we can add to our Ionic Framework apps to make them look even better.

This post is part of the "Mastering Ionic Framework" series which deep dives into Ionic advanced stuff. Don't be afraid, if you are new to Ionic 5, I strongly recommend you to read our What's new in Ionic 5 tutorial.

At IonicThemes we are big fans of Learning by example, that's why all our Ionic tutorials include complete and free code examples that you can reuse in your Ionic projects. We strive to create the best content for Ionic Framework, both tutorials and templates, to help the Ionic community succeed.

You can download all the source code of this ionic 5 tutorial by clicking the GET THE CODE button from above. Also, we published an online demo of what we are going to build for this Ionic Angular Navigation Guide.

Empowering developers to achieve a free life through coding is what keep us going everyday, and seeing developers succeed using our tutorials and templates really fulfill us.

We can help you create better apps with our detailed and professional templates, crafted with love and dedication. Please check them out and let us know if you have any questions or feedback.

Understanding the move from Ionic Navigation to the Angular Router

Navigation is one of the most important parts of an app. Solid navigation patterns help us achieve great user experience while a great router implementation will ease the development process and at the same time make our apps discoverable and linkable.

Navigation is indeed one of the most important elements of user experience that every developer must include on its checklist while creating any kind of app. A bad navigation can frustrate users to an extent that they end up uninstalling the app and even posting a negative review about your app on the app store.

Find more information about how to design a good navigation strategy for your Ionic Application.

What was missing with the previous Ionic Navigation?

Ionic 2 and 3 ditched URL based navigation in favor of a more app-like push/pop implementation. While this approach was super aligned with how native frameworks (iOS, Android) work, and also felt more intuitable to develop with, it completely left out of the table mobile web scenarios.

With the increasing popularity and hype of Progressive Web Apps, this became even more evident. If you are building for the web, then it's super important for your app to be Discoverable (enable search engines find, crawl and index your app pages) and Linkable (pages should be linked together to showcase the content structure and also be easily shareable via URLs). These are 2 of the 10 principles an apps needs to comply in order to be considered a PWA.

Angular Router to the rescue

As the new Ionic 5 position itself as the best tool to build Progressive Web Apps, and the Discoverable and Linkable principles are fundamental to PWAs, it is clear why the Ionic Navigation relies on the Angular Router.

The Angular Router is a solid, URL based navigation library that eases the development process dramatically and at the same time enables you to build complex navigation structures. In addition, the Angular Router is also capable of Lazy Loading modules, handle data through page transitions with Route Resolvers, and handling Route Guards to fine tune access to certain parts of your app. If you want to learn more about Angular routers I suggest you to check the this Angular Tutorial: Learn Angular from scratch step by step.

To be honest, I'm really happy the Ionic team decided to adopt this router for Ionic Angular applications.

If you want to start creating your Ionic Framework app but don't know how to start, I suggest you to try Ionic 6 Full Starter App. It's an ionic 5 app that you can use to jump start your Ionic app development and save yourself hundreds of hours of design and development.

It includes more than 100 carefully designed views and components. It has full support for PWA with Angular and LOTS OF NAVIGATION USE CASES.

ionic starter app

Typically as you keep building new features, the overall application bundle size will continue to grow. At some point you'll reach a tipping point where the application takes a long time to load.

Instead of loading every module at once when the app starts, by organizing your application into modules, it's really easy to make feature modules load lazily and enjoy these benefits:

  • Load feature areas of the app only when the user wants to access them.
  • Speed up load time for users that only visit certain areas of the app.
  • Continue expanding the features of the app without increasing the size of the initial load bundle by using lazy loaded modules.

Just remember that some modules (like the AppModule, typically your main module) must be loaded from the beginning (eagerly loaded). But others (like feature modules) can and should be lazy loaded.

When the router navigates to a lazy loadable route it dynamically loads the module assigned to that route. The lazy loading in Ionic happens just once, when the route is first requested. For subsequent requests, the module and its routes will be available immediately. Let's see an example:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
  },
  {
    path: 'home',
    loadChildren: './home/home.module#HomePageModule'
  },
  {
    path: 'lazy-loaded',
    loadChildren: './lazy-loaded/lazy-loaded.module#LazyLoadedPageModule'
  }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes)
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Note: You don't have to neither import nor reference the lazy loaded modules, just use the route loadChildren property. To be clear, what I mean is that you don't have to include an import statement for the lazy module on the top of the page, and you don't have to include the LazyLoadedPageModule in the NgModule's imports array.

When it comes to Lazy Loading in Ionic and Angular we can choose between the following strategies.

Eagerly Loading modules alternative

Although in my opinion this approach is not recommended, it may be a viable option for small applications because at the first hit of the application all the modules are loaded and all the required dependencies are resolved.

Eagerly loaded modules are loaded before application start, right on the first hit. To load a feature module eagerly, we need to import that module and specify the component property alongside the route path property (instead of using the loadChildren property seen in the examples above).

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { EagerlyLoadedPageModule } from './eagerly-loaded/eagerly-loaded.module';
import { EagerlyLoadedPage } from './eagerly-loaded/eagerly-loaded.page';

const routes: Routes = [
{
  path: '',
  redirectTo: 'home',
  pathMatch: 'full'
},
{
  path: 'home',
  loadChildren: './home/home.module#HomePageModule'
},
{
  path: 'eagerly-loaded',
  component: EagerlyLoadedPage
},
{
  path: 'lazy-loaded',
  loadChildren: './lazy-loaded/lazy-loaded.module#LazyLoadedPageModule'
}
];

@NgModule({
imports: [
  RouterModule.forRoot(routes),
  EagerlyLoadedPageModule
],
exports: [RouterModule]
})
export class AppRoutingModule {}

Pre-loading: loading feature modules in the background

Previously, in this ionic navigation tutorial, we learned how to lazy load modules on-demand. You can also go a step further and load modules asynchronously with preloading.

This may seem what our Angular apps have been doing all along. Not quite. As we mentioned before we are eagerly loading the AppModule when the application starts and we are lazy loading the LazyLoadedPageModule when the user navigates to that page.

Preloading is something in between. It helps you achieve async preloading without relying on the user to click a link. It's super useful for secondary feature modules that you are almost certain a user will visit within minutes of launching the app. Ideally, the app would launch with just the main AppModule and then, almost immediately, preload secondary feature modules in the background. This way, by the time the user navigates to those secondary feature modules, they will be loaded and ready to go.

Out of the box Angular Router provides two strategies:

  • NoPreloading: no modules are preloaded, this is the default behaviour.
  • PreloadAllModules: all modules configured using the loadChildren property will be preloaded.

Adding one of these preloading strategies with Angular is really easy, just add the correspondent import statement and some extra configurations to the RouterModule registration.

import { PreloadAllModules } from '@angular/router';

@NgModule({
imports: [
  RouterModule.forRoot(routes,
  {
    preloadingStrategy: PreloadAllModules
  }),
  EagerlyLoadedPageModule
],
exports: [RouterModule]
})
export class AppRoutingModule {}

Preloading every lazy loaded module can be a good solution in many situations, but it's important to mention that this isn't always the case. Especially if we are on mobile devices and over slow connections. That's why you may choose to preload only certain feature modules based on user metrics and other business and technical factors.

You can reduce over fetching and avoid aggressive prefetching by controlling what and how the router preloads by implementing a custom preloading strategy.

To showcase this feature, let's code a custom strategy that only preloads routes whose data.preload flag is set to true.

We will name this custom preloading strategy FlagPreloadingStrategy

import { PreloadingStrategy, Route } from '@angular/router';

import { Observable, of, timer } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

export class FlagPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: Function): Observable<any> {
    // Check if we want to load it right away or with a delay
    const loadRoute: Function = (delay) => {
      if (delay) {
        // emit a value after 5 seconds, then complete
        return timer(5000)
        .pipe(
          // Merge the value emitted by the timer and then emmit the load() function
          mergeMap(_ => load())
        );
      } else {
        return load();
      }
    };

    return route.data && route.data.preload ? loadRoute(route.data.delay) : of(null);

    // If you opt-out of the delay functionality then return this instead
    // return route.data && route.data.preload ? load() : of(null);
  }
}

Notice how we went a step further and added a delay option. When configuring your routes, if you set the delay property to false then the FlagPreloadingStrategy will call the load() function right away.

If delay is set to true, then we will return an observable that emits the load() function after an interval.

Next, you should import the custom preloading strategy, configure the RouterModule to use this custom strategy instead, and don't forget to include the FlagPreloadingStrategy in the NgModule providers array.

import { FlagPreloadingStrategy } from './utils/flag-preloading-strategy';

{
  path: 'lazy-loaded',
  loadChildren: './lazy-loaded/lazy-loaded.module#LazyLoadedPageModule'
},
{
  path: 'pre-loaded',
  loadChildren: './pre-loaded/pre-loaded.module#PreLoadedPageModule',
  data: {
    preload: true,
    delay: false
    }
  },
  {
  path: 'pre-loaded-with-delay',
  loadChildren: './pre-loaded-with-delay/pre-loaded-with-delay.module#PreLoadedWithDelayPageModule',
  data: {
    preload: true,
    delay: true
  }
}

@NgModule({
imports: [
  RouterModule.forRoot(routes,
  {
    preloadingStrategy: FlagPreloadingStrategy
  }),
  EagerlyLoadedPageModule
],
exports: [RouterModule],
providers: [FlagPreloadingStrategy]
})
export class AppRoutingModule {}

Advanced Preloading Strategies

Hold on! This wouldn't be an "in-depth" ionic navigation tutorial if we just fall in with shallow use cases.

Inspired by the custom Angular decorator approach followed by Ely Lucas while exploring different preloading strategies I thought it was worth including this advanced use case.

From a preloading standpoint, it is ideal to stay one step ahead of the user and preload any routes he might try to go to next. For example, when a user visits a listing screen, you could preload the details view so it is ready to go.

We will go the extra mile and create a custom decorator so we can programatically trigger the preloading of routes from within our Angular components/pages. This way you can easily preload the pages you think the users will navigate to in the near term.

This custom pre-loading strategy instead of calling the load function immediately for each route, will store it in a dictionary for future use.

import { PreloadingStrategy, Route } from '@angular/router';

import { Observable, of } from 'rxjs';

export class DecorativePreloadingStrategy implements PreloadingStrategy {
  // Keep track of all the preloadable routes and their corresponding load() function
  routes: { [name: string]: { route: Route; load: Function } } = {};

  // This function will get called for every preloadable route (route that has the loadChildren property)
  preload(route: Route, load: Function): Observable<any> {
    if (route.data && route.data.name) {
      this.routes[route.data.name] = {
        route,
        load
      };
    }
    return of(null);
  }

  // We will call this function manually later
  preLoadRoute(name: string) {
    const route = this.routes[name];
    if (route) {
      route.load();
    }
  }
}

We also need a mechanism to identify angular routes by name so we can reference them later. Add the following configuration in your routing module:

{
  path: 'listing',
  loadChildren: './listing/listing.module#ListingPageModule',
  data: {
    name: 'ProductsListing'
  }
  },
  {
  path: 'details',
  loadChildren: './details/details.module#DetailsPageModule',
  data: {
    name: 'ProductDetails'
  }
}

As Angular PreloadingStrategies are just services, they can be injected into our Ionic Components like any other service. Just import and inject a reference to our custom loader in our ListingPage:

import { Component, OnInit } from '@angular/core';

import { DecorativePreloadingStrategy } from '../utils/decorative-preloading-strategy';

@Component({
  selector: 'app-listing',
  templateUrl: './listing.page.html',
  styleUrls: ['./listing.page.scss'],
})
export class ListingPage implements OnInit {

  constructor(private loader: DecorativePreloadingStrategy) { }

  ngOnInit() {
    this.loader.preLoadRoute('ProductDetails');
  }
}

Finally we just need to call our preLoadRoute() method with the name of the route we want to preload. The method will lookup the specified route in the dictionary and if found, it will call that route's load() method.

This way, when the ListingPage loads, it will start preloading the ProductDetailsPage.

If you need help with the navigation of your Ionic app, I suggest you to take a look at Ionic 6 Full Starter App - The most complete Ionic Template. It will save you LOTS of development and design time. It has different types of navigations that you can reuse in your apps.

Auth in Ionic 5
Ionic 5 authentication
Ionic 5 log in

The extra mile

Although the previous implementation works in Ionic and Angular, we could clean the code a bit by abstracting the loading logic into a custom Angular decorator which can be added to our ionic pages from where we want to preload possible upcoming pages.

What we are going to do in this custom angular decorator is basically hookup to the original ngOnInit() function, inject our custom preloading strategy (which essentially is an Angular service), and finally call the loader preLoadRoute() method to load the desired route.

import { DecorativePreloadingStrategy } from './decorative-preloading-strategy';
import { IPreloadingComponent } from './preloading-component.interface';

export function PreLoad(page: string): ClassDecorator {
  // Decorator Factory
  return function(target: Function) {
    // Get a reference to the original target ngOnInit function
    const targetNgOnInit: Function = target.prototype.ngOnInit;

    // Override target ngOnInit function
    // Note: Do not use arrow function to redefine the target ngOnInit (see: https://stackoverflow.com/a/52986447/1116959)
    target.prototype.ngOnInit = function (...args) {
      // 'this' refers to the original Component scope
      const targetComponent: IPreloadingComponent = this;

      // Get a reference to the injector in the target Component
      const targetInjector = targetComponent.injector;
      if (targetInjector) {
        // Inject the custom preloading strategy (which is essentially a service)
        const loader = targetInjector.get(DecorativePreloadingStrategy);

        loader.preLoadRoute(page);
      }

      // Check if we have an ngOnInit function defined in the original target, and 'merge' it with this re-implementation
      if (targetNgOnInit) {
        targetNgOnInit.apply(this, args);
      }
    };
  }
}

Then use our custom decorator in the page component like this:

import { Component, OnInit, Injector } from '@angular/core';

import { PreLoad } from '../utils/pre-load.decorator';
import { IPreloadingComponent } from '../utils/preloading-component.interface';

@Component({
  selector: 'app-listing',
  templateUrl: './listing.page.html',
  styleUrls: ['./listing.page.scss'],
})
@PreLoad('ProductDetails')
export class ListingPage implements OnInit, IPreloadingComponent {

  constructor(public injector: Injector) { }

  ngOnInit() { }
}

Note: We created an extra interface to ensure the component from where we want to preload other routes has everything it needs and won't fail. Basically what it needs are both a reference to the Angular Injector (that's what we use to inject the custom preloading strategy in the decorator above), and a definition of the ngOnInit() method.

import { Injector } from '@angular/core';

export interface IPreloadingComponent {
  injector: Injector;
  ngOnInit: Function;
}

How custom a preloading strategy can be?

Ok, if you are crazy enough, you can try this ultra-advanced technique which combines machine learning and Google Analytics data to guess which will be the next page the user will navigate to and load that route using a custom preload strategy. If you are interested, please follow Minko Gechev's post on how to add a quicklink preloading strategy to your Angular project.

Angular Route Guards are very useful to control whether the user can navigate to or away from a given page.

For example, in our Ionic app, we may want some routes to only be accessible to logged in users or users who pay a premium subscription. We can use route guards to check these conditions and control access to those routes.

We can also use Route Guards to control whether a user can leave a certain route. This can be useful, for example, if the user has typed information into a form without submitting it. In this cases, we may want to alert users when they attempt to leave the route without submitting or saving the information.

The Angular router supports the following guard interfaces:

  • CanActivate to mediate navigation to a specific route.
  • CanActivateChild to mediate navigation to a child route.
  • CanDeactivate to mediate navigation away from the current route.
  • Resolve to perform route data retrieval before route activation.
  • CanLoad to mediate navigation to a feature module which is loaded asynchronously.

It's worth mentioning that you can have multiple guards at every level of a routing hierarchy. In this Ionic 5 Navigation tutorial we will see how to use each of these Angular guards.

CanActivate: Gatekeeping navigation to special routes

Applications often restrict access to a feature area based on who the user is. You could create an Ionic app which allows access to some pages only to authenticated users or users with a specific role. Or maybe, you might want to block access to premium content until the user enrolls in a paid subscription.

The Angular CanActivate guard is the tool to manage these navigation business rules.

Depending on the value returned by the guard, the behavior the router will follow. If it returns true, the navigation process continues, and if it returns false, the navigation process stops and the user stays put.

Most recently (Angular version >= 7.1.0), the guard can now return a UrlTree. In this scenario, the current ionic navigation will be canceled and a new one to the UrlTree returned will be initiated. This is particularly interesting if you want to redirect the user from within the guard. Say for example, if a user fails an authentication guard, you may want to redirect that user directly to the login page.

To illustrate these use cases in our ionic example, we will add two new routes in the routing module:

{
  path: 'can-activate',
  loadChildren: './can-activate/can-activate.module#CanActivatePageModule'
},
{
  path: 'redirect-to',
  loadChildren: './redirect-to/redirect-to.module#RedirectToPageModule'
}

Then, inside the can-activate module, add the new CanActivateGuard and configure its usage for that route:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';

@Injectable()
export class CanActivateGuard implements CanActivate {

  constructor(private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {
    console.log('CanActivateGuard [canActivate] called');
    const url = 'redirect-to';
    const tree: UrlTree = this.router.parseUrl(url);

    return tree;
    // return true;
    // return false;
  }
}
import { CanActivatePage } from './can-activate.page';
import { CanActivateGuard } from './can-activate.guard';

const routes: Routes = [
  {
    path: '',
    component: CanActivatePage,
    canActivate: [ CanActivateGuard ]
  }
];

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild(routes)
  ],
  declarations: [CanActivatePage],
  providers: [ CanActivateGuard ]
})
export class CanActivatePageModule {}

Notice how the CanActivateGuard can return multiple values. For this example, everytime the user tries to access the /can-activate route we will cancel that navigation and start a new one to /redirect-to route.

Angular route guards

The example above is a dummy implementation. Ideally the CanActivateGuard would use an AuthService to check whether the user is logged in or not and return a value accordingly.

CanDeactivate: Double check before leaving a route

As we mentioned before, we use this angular guard to decide if a route can be deactivated, for example let's add a security check in our ionic example app to verify whether a user wants to leave a certain route if it has unsaved changes.

Angular guards
import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { Observable } from 'rxjs';

import { IDeactivatableComponent } from '../utils/deactivatable-component.interface';

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<IDeactivatableComponent> {
  canDeactivate(
    component: IDeactivatableComponent,
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    return component.canDeactivate();
  }
}

The canDeactivate() method provides a reference to the current instance of the component, this way we can ask the component that's getting deactivated if there are any unsaved changes and prompt the user to confirm the action.

import { Component } from '@angular/core';

import { Observable } from 'rxjs';

import { IDeactivatableComponent } from '../utils/deactivatable-component.interface';

@Component({
  selector: 'app-can-deactivate',
  templateUrl: './can-deactivate.page.html',
  styleUrls: ['./can-deactivate.page.scss'],
})
export class CanDeactivatePage implements IDeactivatableComponent {

  constructor() { }

  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
    // For simplicity we use a flag. You should implement the logic to figure out if there are any unsaved changes or not
    const areUnsavedChanges = true;

    let canDeactivate = true;

    if (areUnsavedChanges) {
      canDeactivate = window.confirm('Are you sure you want to leave this page?');
    }

    return canDeactivate;
  }
}

Notice how both the CanActivate guard and the component we want to check if it can be deactivated both rely on a IDeactivatableComponent.

This way we make our guard reusable by avoiding specifying a concrete component. The guard shouldn't know the details of any component's deactivation method. It only needs to make sure the component has a canDeactivate() method to call.

import { Observable } from 'rxjs';

export interface IDeactivatableComponent {
 canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

Finally, don't forget to add the new CanDeactivateGuard reference and configure its usage on the can-deactivate module.

import { CanDeactivatePage } from './can-deactivate.page';
import { CanDeactivateGuard } from './can-deactivate.guard';

const routes: Routes = [
  {
    path: '',
    component: CanDeactivatePage,
    canDeactivate: [CanDeactivateGuard]
  }
];

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild(routes)
  ],
  declarations: [CanDeactivatePage],
  providers: [CanDeactivateGuard]
})
export class CanDeactivatePageModule {}

CanLoad: Preventing unauthorized loading of feature modules

This guards are slightly different from the CanActivate guards in that they prevent the loading of the module all together.

Suppose that in your ionic 5 app, you want to use a CanActivate guard to prevent unauthorized users from accessing a specific page/feature area. If users are not authorized, they will be redirected to another ionic page. In this scenario, although the usability looks the same to the user, the router is still loading the feature area you are protecting even if the user can't visit any of its components.

Ideally, you'd only load the feature area if the user is authorized to access it.

import { Injectable } from '@angular/core';
import { CanLoad, Route, UrlSegment } from '@angular/router';

import { Observable } from 'rxjs';

@Injectable()
export class CanLoadGuard implements CanLoad {

  canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> | Promise<boolean> | boolean {
    // return true;
    return false;
  }
}

This Angular guard must be referenced and configured when loading the module. In our ionic navigation example, that means we have to add it to the routing module like this:

import { CanLoadGuard } from './can-load/can-load.guard';

{
  path: 'can-load',
  loadChildren: './can-load/can-load.module#CanLoadPageModule',
  canLoad: [CanLoadGuard]
}

@NgModule({
  imports: [
    RouterModule.forRoot(routes,
    {
      preloadingStrategy: DecorativePreloadingStrategy
    }),
    EagerlyLoadedPageModule
  ],
  exports: [RouterModule],
  providers: [
    DecorativePreloadingStrategy,
    CanLoadGuard
  ]
})
export class AppRoutingModule {}

Note: The CanLoad guard takes precedence over the preload strategy we mentioned earlier. This means that if you apply a preloading strategy, the feature areas protected by CanLoad guards won't be pre-loaded. If you want to preload a module and guard against unauthorized access, remove the CanLoad guard and rely on the CanActivate guard alone.

Angular Route Resolves are a special kind of route guards. They enable us to pre-fetch data from the server before navigating to a route. This way, the data is ready the moment the route is activated.

In Ionic 6 Full Starter App, we use Route Resolvers in almost every page of the app. This technique enables us to avoid displaying a blank component while waiting for the data to load, which translates in a better user experience and perceived performance.

If we weren't to use angular route resolvers, we would have to wait until the route got activated to fetch the data from the server and consequently displaying critical information for that page slower.

Route resolvers also allow you to handle errors before navigating to the component. There's no point in navigating to a product details page if that product ID doesn't have a record on the database. It'd be better to send the user back to the Products Listing page and show him a proper error message.

Alright, tighten your seatbelts as we are gonna explore in depth how to make the most out of Angular route resolves while also improving the user experience involved in handling data through routes in an Ionic application.

Improving the User Experience and Perceived Performance in Ionic Apps

Data flow is key to the overall User Experience

The apps we make are used by humans, and contrary to machines we do not measure time precisely, we perceive time. People perceive time differently depending on how anxious they feel and if they are on the move or at home.

Websites and apps can and will load slowly, and even if they are optimized, 30% of users will still perceive them to be slower than they really are.

This is why we need to start thinking on strategies to hack user perception and make our websites and apps feel faster.

Why is the Perceived Performance so important?

Perceived Performance is a measure of how quick a user thinks your site or app is, and that's often more important than it's true speed.

You need to strive for getting meaningful content to your users as quickly as possible. Then, once they have something they can read or interact with, add more content on top. Throw in the rest of the styles, fetch the JavaScript needed to obtain a fancier experience, but always provide your users with something to do other than stare at a motionless screen!

On the whole, there is a study that says we perceive a delay on loading which is 80 milliseconds more than the reality. So if you are left waiting around, things will feel even longer.

Loading...

As we mentioned before, showing a blank screen in our ionic apps is bad, keeping the user on hold without any feedback, but showing a spinner is equally flawed.

You can see that in the ionic example shown in the following video, load times feel longer because the user is left waiting for content. Also, the loader indicator shows the app in a “thinking” state rather than “working”.


A better alternative: Filling the screen with staggered interactions

Instead of using the old-fashioned loading spinner in our Ionic application, we will fill the screen using a mixture of skeleton loading content, contextual metadata and image placeholders, with a nice and fresh animation. This way, we can occupy a lot of the user's time and make the whole waiting experience feel faster. The idea of this alternative is to give context to the users of what's going on and keep them engaged while loading stuff. In the following video you can see some of the app skeletons we created for Ionic 6 Full Starter App.


UI Skeletons, Ghost Elements, Shell Elements, they are all the same! Think of them as cool content placeholders that are shown where the content will eventually be once it becomes available. Learn the importance of adopting the App Shell pattern in your ionic apps and how to implement it using Ionic, Angular and some advanced CSS techniques.

Now that we have a clear understanding of the problem, let's code a solution to this perceived performance issue using Ionic 5 and the Angular Router.

Making Angular Route Resolvers and Skeleton Screens work together

As we mentioned before, we need some sort of skeleton content placeholders to improve our ionic app's perceived performance. This is also aligned with the App Shell architecture Google suggests to use when building Progressive Web Apps.

We will use the following words as synonyms to refer to the same concept: App Shell, Skeleton screens or Ghost loading. They all refer to the same architecture pattern.

In my opinion, a proper Skeleton Loading implementation consists of two principles:

  1. Do not block page transitions.
  2. Show content placeholders while loading data for that page.

In this Ionic navigation tutorial, we have already talked about staggered interactions using Skeleton components to improve perceived performance. Using Angular Route Resolvers to fetch data while navigating between routes is also fundamental to our solution, let's see how to combine them.

Tweaking how the Angular Router resolves data

Why plain Resolvers are not enough

If we would use Angular Route Resolvers out of the box in our Ionic app, we could easily show a spinner using the Ionic Loading Controller while fetching data from the backend. Once the data is resolved, the navigation transition to the ionic page and the availability of the data are immediate. This leaves no reason to use skeleton screens as we have the data to fill the page content as soon as the page gets activated. However, the user will see a blocked UI with just a spinner indicator while waiting for the data to be loaded.

This happens because, by design, Angular Route Resolvers won't transition to the page until the resolved Observable completes. That prevents us from returning an open stream of data from our Resolver. This is not something new, people have been requesting this kind of functionality to be added to the Angular Router for quite some time.

Use case: Let's suppose our backend is slow and takes 5 seconds to fetch data and return it to the client. The expected behavior for that scenario is that the page transition will be blocked for 5 seconds until the server sends data back to the client. You can see this use case illustrated into an Ionic 5 mobile app in the following gif:

angular resolvers

Remember that the full code of these examples can be found by clicking the GET THE CODE button at the top of the page.

Non blocking Angular 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.

resolve() {
// Resolver using a ReplySubject that emits the base Observable and then completes
const subject = new ReplaySubject();
subject.next(baseObservable);
subject.complete();
return subject;
}
resolve() {
  // Resolver using an Observable that emits the base Observable and then completes
  const observable = Observable.create((observer) => {
    observer.next(baseObservable);
    observer.complete();
  });
  return observable;
}
resolve() {
  const promise = new Promise((resolve, reject) => {
    resolve(baseObservable);
  });
  return promise;
}

I like the Promise approach as it's more straightforward, you end up resolving an instant promise for the base Observable.

In our ionic navigation example app you can see this use case illustrated.

Angular resolvers

This feature would be very useful in many scenarios, one that comes to my mind is for example when using real-time data updates from Firestore with @angular/fire. It would be nice to keep getting updates from the resolved route data.

Note: The trade-off of this approach is that the waiting time gets passed to the page component you are navigating to. This also means that you will be responsible for unhandled errors that may cause navigating to unavailable pages (for example navigating to a non existing product details page after following an invalid product ID).

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 skeleton model while we wait for the real data from the backend, and once we have the real data, progressively translate the skeleton model to the real data.

This will help us fixing the waiting time issue caused by non-blocking resolvers while fetching data after navigating to a page.

Ionic skeleton screens

For this solution we will use a mix of:

  • Shell Elements as reusable, animated content placeholders
  • Mock Shell Models to provide a clear interface that both the shell and view data will adhere
  • And a Shell Provider to handle the stream of data

Creating Skeleton Layouts in Ionic Framework

We created a series of skeleton elements for Ionic that will help us build a variety of UIs that progressively translate from the shell model loading state to the final state displaying real data. Feel free to reuse this elements on your Ionic projects. Remember you can get all the source code of this ionic navigation tutorial by clicking the GET THE CODE button at the top of the page.

Also, if you want to see a real Ionic 5 app using this skeleton components, you should check Ionic 6 Full Starter App which features a lot of beautiful skeleton screens that you can reuse into your Ionic projects.


The idea behind these ghost 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 real data.

In this ionic 5 navigation tutorial, I just used a simplified version of the ghost elements using just CSS to showcase the use case. Full featured version of the components can be found in our latest Ionic 5 starter template under the showcase section.

<div class="image-shell">
  <img [src]="item?.image" alt="Sample Image"/>
</div>

<p class="text-shell" [ngClass]="{'text-loaded': item?.description}">
{{ item?.description }}
</p>
.image-shell {
  position: relative;
  padding-bottom: 100%;
  height: 0px;

  // The animation that goes beneath the masks
  &::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background:
      linear-gradient(to right, #EEE 8%, #DDD 18%, #EEE 33%);
    background-size: 800px 104px;
    animation: animateBackground 2s ease-in-out infinite;
  }

  & > img {
    position: absolute;
    top: 0px;
    left: 0px;
    right: 0px;
    bottom: 0px;
    width: 100%;
    height: 100%;

    &[src=""],
    &[src="null"] {
      display: none;
    }
  }
}

.text-shell {
  position: relative;

  // The animation that goes beneath the masks
  &::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background:
      linear-gradient(to right, #EEE 8%, #DDD 18%, #EEE 33%);
    background-size: 800px 104px;
    animation: animateBackground 2s ease-in-out infinite;
  }

  &.text-loaded {
    &::before,
    &::after {
      background: none !important;
      animation: 0 !important;
    }
  }
}

p.text-shell {
  // The masks
  &::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background-repeat: no-repeat;
    background-image:
      /* First line: 95% width grey, 5% white mask */
      linear-gradient(to right, transparent 95% , #FFF 95%),
      /* Separation between lines (a full width white line mask) */
      linear-gradient(to right, #FFF 100%, #FFF 100%),
      /* Second line: 65% width grey, 35% white mask */
      linear-gradient(to right, transparent 65% , #FFF 65%);

    background-size:
      /* First line: 100% width, 16px height */
      100% 16px,
      /* Separation between lines: a full width, 3px height line */
      100% 3px,
      /* Second line: 100% width, 16px height */
      100% 16px;

    background-position:
      /* First line: begins at left: 0, top: 0 */
      0 0px,
      /* Separation between lines: begins at left: 0, top: 16px (right below the first line) */
      0 16px,
      /* Second line: begins at left: 0, top: (16px + 3px) (right below the separation between lines) */
      0 19px;
  }
}

@keyframes animateBackground {
  0%{
    background-position: -468px 0
  }

  100%{
    background-position: 468px 0
  }
}

I won't go into details on this because we have written an in-depth tutorial about adding App Skeleton screens to an Ionic app. Please check it for further details.

Creating Data Models to use in our Ionic App Skeleton architecture

We will use data models to define a clear structure of the data we will display in our views. Shell models provide empty/null objects that mock the data architecture of our UI.

export class SampleShellModel {
  image: string;
  title: string;
  description: string;
  }

  export class SampleShellListingModel {
  items: Array<SampleShellModel> = [
    new SampleShellModel(),
    new SampleShellModel(),
    new SampleShellModel()
  ];

  constructor(readonly isShell: boolean) { }
}
  

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 ShellProvider class that holds onto the original Observable used to request your backend data.

We are going to use a BehaviorSubject that will emit two values (just like a stream of data), one mock object emitted instantly (from which we can build up the skeleton components) and then a second value with the business data to fill the page content (once the data is ready).

We just need to init the BehaviorSubject with the mock shell model, and immediately after, fetch our backend data and emit the response to the same BehaviorSubject. (If you are new to rxjs BehaviorSubject I suggest you to read this post).

export class ShellProvider<T> {
  private _observable: Observable<T>;

  // A Subject that requires an initial value and emits its current value to new subscribers
  // If we choose a BehaviorSubject, new subscribers will only get the latest value (real data).
  // This is useful for repeated use of the resolved data (navigate to a page, go back, navigate to the same page again)
  private _subject: BehaviorSubject<T>;

  // We wait on purpose 2 secs on local environment when fetching from json to simulate the backend roundtrip.
  private networkDelay = 2000;

  constructor(shellModel: T, dataObservable: Observable<T>) {
    // Set the shell model as the initial value
    this._subject = new BehaviorSubject<T>(shellModel);

    const delayObservable = of(true).pipe(
      delay(this.networkDelay)
    );

    dataObservable.pipe(
      first() // Prevent the need to unsubscribe because .first() completes the observable
    );

    // Put both delay and data Observables in a forkJoin so they execute in parallel so that
    // the delay caused (on purpose) by the delayObservable doesn't get added to the time the dataObservable takes to complete
    const forkedObservables = forkJoin(
      delayObservable,
      dataObservable
    )
    .pipe()
    .subscribe(([delayValue, dataValue]: [boolean, T]) => {
      this._subject.next(dataValue);
    });

    this._observable = this._subject.asObservable();
  }

  public get observable(): Observable<T> {
    return this._observable;
  }
}

Following this approach we would have instant transitions while navigating through our ionic framework application, and when a new page gets activated we would show some ghost components while fetching real business data from the backend.

Wrapping up: Ionic Skeleton and Angular Routes

I know it wasn't straightforward, but we achieved the user experience we were looking for!

Our service will look like this:

private getData(): Observable<any> {
  const dataObservable = this.http.get<any>('./assets/sample-data/page-data.json').pipe(
    tap(val => {
      console.log('getData STARTED');
    }),
    delay(5000),
    finalize(() => {
      console.log('getData COMPLETED');
    })
  );

  return dataObservable;
  }

  private getDataWithShell(): Observable<SampleShellListingModel> {
  // Initialize the model specifying that it is a shell model
  const shellModel: SampleShellListingModel = new SampleShellListingModel(true);
  const dataObservable = this.getData();

  const shellProvider = new ShellProvider(
    shellModel,
    dataObservable
  );

  return shellProvider.observable;
}

Our resolver remains almost intact:

resolve() {
  // Get the Shell Provider from the service
  const shellProviderObservable = this.getDataWithShell();

  // Resolve with Shell Provider
  const observablePromise = new Promise((resolve, reject) => {
    resolve(shellProviderObservable);
  });
  return observablePromise;
}

While building our latest and most complete Ionic Framework Starter Template, I found this user experience roadblock challenging. That's why we strived to find the best solution.

I know the approach we followed may seem overwhelming or over-engineered, but it was the best solution I came up with.

Please, if you found a better alternative let me know in the comments below.

That was intense!

Now you have a better understanding of the new Ionic Navigation using the Angular Router. We did an in-depth analysis of all the three main features:

  • Lazy Loading
  • Route Guards
  • Route Resolves + UX bonus (App Skeleton FTW!)

I did my best to be as clear as possible, but I know we mentioned a lot of new concepts and complex stuff that are not always easy to explain. If you have any feedback or suggestions, please let me know in the comments below.

Navigation is one of the most important elements of user experience in mobile app. For this reason is so important to keep the best practices for navigation design to ensure that people will be able to use and find the most valuable features in your app.

As I mentioned before, we wrote another post about improving Ionic apps UX with app skeleton loading components. In that post we focused more on some advanced CSS techniques to animate the ghost components. It's completely complementary to this one, so if you are intrigued by the nifty details of UI micro interactions and animations in Ionic and Angular, I suggest you check it out.

Really enjoyed writing this Ionic tutorial, see you in the next post!