/mar 23, 2015

Improving the Angular Single-Page Application Loading Experience

By Kyle Clark

Waiting. Still waiting. "Why isn't this working? It's not even moving!" Everyone has experienced it. Painfully waiting for a web application to load. Those seemingly never-ending moments without any sign of movement is right up there with the agony of sitting in a rush hour traffic jam. No clear sign of what's happening. "Let's go already!" The experience will take users back to the dark days of browsing the web on a 56k connection. Shudders.

The perception of performance is just as important, if not more-so, than the actual performance. Users want to know what is going on, so tell them already! By following the concepts we applied in our application, you will greatly improve the loading experience in your Angular single-page application.

Single-page applications are notorious for a large, upfront loading hit. Angular is no exception. But it doesn't stop there at the initial loading. API calls for data, sometimes massive amounts, are likely to be requested on route changes in the application. Leading to more, you guessed it, waiting. Just don't make your users hold their breathe. Keep them engaged and improve your Angular single-page application loading experience.

Response times in any application need to be snappy, especially in an era of instant gratification and proliferation of distractions. Keeping a user's attention is critical. Continuous feedback on the response state of the application should be provided to the user.

<body>

  <!-- ui-view container for your angular app -->
  <div ui-view=""></div>

  <!-- splash container for app loading -->
  <div class="fade-it" ng-if="isAppLoading">

    <div class="full-screen text--center bg--alternate--light zIndex-10--loadingPage">

      <h3 class="mt++">SRC:CLR</h3>
      <div spinner></div>

    </div>

  </div>

</div>

The container above includes some of our internal css classes as well as a fancy spinner directive for the animation. The fade-it class is also used to initiate a fade-in and fade-out animation on the display of the container. The ng-if declaration is how we control when the container should be displayed. On the inner div container, text--center and bg--alternate--light are purely for formatting the text position and styling the background color. Take note of the full-screen and zIndex-10--loadingPage classes in particular:

.full-screen {
  position: fixed;
  height: 100%;
  width: 100%;
  top: 0;
  left: 0;
}

.zIndex-10--loadingPage {
  z-index: 1000;
}

Basic CSS. This will stretch your splash container over the top of your web page when isAppLoading is truthy. Most importantly, let's look at how we handle the logic for isAppLoading.

After we declare all of our third party library code modules inside Angular's app.js, our core config.js is processed. In the config, we set isAppLoading to trigger the splash loading container display, while the rest of our Angular modules are being instantiated.

angular.module('SC.config', [])
  .config(function () {

    // More to be covered in here later

  }).run(function ($rootScope) {

    // App is loading, so set isAppLoading to true and start a timer
    $rootScope.isAppLoading = true;
    $rootScope.startTime = new Date();

  });

And just like that, once we set $rootScope.isAppLoading true, the splash html container will instantaneously appear for the user, rather than a 'white screen of death.'


However, if the application were to quickly finish loading, we need to prevent the splash container from flickering. The startTime new Date is defined to address the edge case by checking the time elapsed on our route success.

// Logic to handle timing of app loading
var diff;

// Check if isAppLoading is truthy
if ($rootScope.isAppLoading) {

  // Find the elapsed difference between the present time and the startTime set in our config
  diff = new Date() - $rootScope.startTime;

  // If 800ms has elapsed, the loading splash can be hidden
  // else create a timeout to hide the loading splash after 800ms has elapsed since the startTime was set
  if (diff > 800) {
    $rootScope.isAppLoading = false;
  } else {
    $timeout(function () {
      $rootScope.isAppLoading = false;
    }, 800 - diff);
  }

}

If you are wondering where the above JavaScript code should live in your application, we use the popular and flexible AngularUI Router for internal routing. AngularUI Router broadcasts four state change events from $rootScope:

$stateChangeStart
$stateChangeSuccess
$stateChangeError
$stateNotFound

These events allow us to capture the state of a route action. For instance, our app loading defaults to /login for users who are not logged in. To handle routing logic, we create a high-level routes-config.js. The routes-config file configures many of our state providers and executes the subscription to the ui-router on state change events. In the $stateChangeSuccess event callback, we add the isAppLoading time elapsed checking code snippet.

Note: You will need to handle similar logic for hiding the isAppLoading splash container on the error state event as needed.


Thus far, we created a splash container as a prompt fill-in for the empty screen on app loading. The next step to improve the user experience is to integrate a loading progress bar on the app loading and route change phases. Angular Loading Bar is an easy-to-use module to utilize in your application (really, about as easy as it gets for 3rd party code). To use the loading bar straight out of the box, so to speak, only requires installing the package via npm or bower and adding the dependency into your app. That's it.

Of course, the default settings did not quite satisfy our requirements. We wanted something more robust; for the loading progress bar to start as soon as the splash container is displayed. In addition, the progress bar should trigger on route changes, except on routes which finish processing in less than 400ms. If a route displays almost instantly, there is no reason to show a loading progress bar status -- in that short of time the user will feel like the application is responding naturally without delay.

Ergo, we developed a customized solution with the advanced usage service of the Angular Loading Bar. After you install the package, add the module dependency cfp.loadingBar into your app. Then, per our code structure, update the base config file with a couple small additions:

angular.module('SC.config', [])
  // Require cfpLoadingBarProvider for configuration
  .config(function (cfpLoadingBarProvider) {

    // Remove loading bar spinner
    cfpLoadingBarProvider.includeSpinner = false;

  // Require cfpLoadingBar
  }).run(function ($rootScope, cfpLoadingBar) {

    // App is loading so auto-set isAppLoading and start a timer
    $rootScope.isAppLoading = true;
    $rootScope.startTime = new Date();

    // Start loading bar for app loading
    cfpLoadingBar.start();

  });

Furthermore, inside our routes-config, we expanded the implementations for handling app loading and routing.

angular.module('SC.routes')
  .config(function () {

    // Where our global routing configuration magic happens

  })
  .run(function ($rootScope, $timeout, cfpLoadingBar) {
    var diff,
        timeoutPromise;

    // Subscribe to broadcast of $stateChangeStart state event via AngularUI Router
    $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams, error) {

      // If app is not already loading (since we started the loading bar in the config with the isAppLoading)
      if (!$rootScope.isAppLoading) {

        // $timeout returns a deferred promise to execute by the defined time of 400ms
        // set isAppRouting true and start loading bar
        // if route success or error takes 400ms or greater, timeout will execute
        timeoutPromise = $timeout(function () {
          $rootScope.isAppRouting = true;
          cfpLoadingBar.start();
        }, 400);

      }

    });

    // Subscribe to broadcast of $stateChangeSuccess state event via AngularUI Router
    $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams, error) {

      // Cancel timeout promise (if it exists) from executing, if route success occurs before the 400ms elapses
      if (timeoutPromise) {
        $timeout.cancel(timeoutPromise);
      }

      // Logic to handle elapsed time of app loading phase else handle app routing
      if ($rootScope.isAppLoading) {
        // Find the elapsed difference between the present time and the startTime set in our config
        diff = new Date() - $rootScope.startTime;

        // If 800ms has elapsed, isAppLoading is set to false
        // else create a timeout to set isAppLoading to false after 800ms has elapsed since the startTime was set
        if (diff > 800) {
          $rootScope.isAppLoading = false;
          cfpLoadingBar.complete();
        } else {
          $timeout(function () {
            $rootScope.isAppLoading = false;
            cfpLoadingBar.complete();
          }, 800 - diff);
        }

      } else if ($rootScope.isAppRouting) {
        // App finished routing, complete loading bar
        $rootScope.isAppRouting = false;
        cfpLoadingBar.complete();
      }

    });

  });

With these modifications, on app loading, the splash container and loading progress bar are both displayed. In route changes, the progress bar kicks off after a route takes 400ms or longer to resolve, and completes itself upon the state success / error event.


The improvements we showcased are only the tip of the iceberg in what can be done to make your application feel more responsive and reactive during loading and routing. Nevertheless, prior to the enhancements, the user would see a blank page on app loading and experience a static delay on long-resolving route changes without any idea indication why the page view was not updating.

Despite unavoidable delays when loading data, visual cues, especially in some form of a status indicator, are vital to keeping the user's attention. Make your loading phases responsive with a feedback loop to users. Immediate response times in an application will empower users to feel as if they are driving the application, and not the other way around.

Related Posts

By Kyle Clark