Angular, webpack and gulp for an SPA: Part III

In Part I, we got webpack to run with gulp, watching our Angular app. In Part II, we added routing with ui-router. Here in Part III, we check out the magic of lazy loading.

What is lazy loading? Typically, with most Single Page Apps (SPAs), what the client actually receives is the index file, downloads the application script(s), and then the client-side framework and routing kick in. From that point on, whenever you navigate to a new ‘page’, the app doesn’t really request a new page — it was all downloaded from the get-go.

Well, this can be wasteful. If your user goes to the User Settings page, she doesn’t need to download app components for other pages. Wouldn’t it be nice if the user could download only what she needs for that current page, and then download other stuff as she needs them?  This is called lazy loading.

Let’s do it with our Angular app.

The entry index.js file:


require('modules/common/components');

// nope, we don't bundle this module in...we want to lazy load it!
// require('modules/captain-list/'); 
 
var startrekApp = angular.module("startrekApp", 
['ui-router', 'oc.lazyLoad', 'startrekApp.common.components'])

.config( ['$stateProvider', '$locationProvider', '$ocLazyLoadProvider', 
function($stateProvider, $locationProvider, $ocLazyLoadProvider) {

/*
   // this is what it was before
   $stateProvider
   .state('captainList', { 
      url: '/captains', 
      templateUrl: 'captainList.html', 
      controller: 'CaptainListController'
   }) 
*/

   // with lazy loading
   $stateProvider
   .state('captainList', {
      url: '/captains',
      templateProvider: ['$q', function($q) {
         var deferred = $q.defer();

         require.ensure([], function() {
            var template = require('html!./captain-list/captainList.html');
            deferred.resolve(template);
         });
         return deferred.promise;
      }],
      controller: 'CaptainListController',
      resolve: ['$q', '$ocLazyLoad', function($q, $ocLazyLoad) {
         var deferred = $q.defer();

         require.ensure([], function() {
            var mod = require('./captain-list');
            $ocLazyLoad.load({
                name: 'cl3.captainList',
            });
            deferred.resolve(mod.controller);
         });

         return deferred.promise;
      }]
   })
   $locationProvider.html5Mode(true);
}]);

Ok, so that’s a lot of new stuff. Let’s take a look at each in turns. Up first, notice we inject the Angular module ocLazyLoad. See, Angular wants to know ALL modules (emphasis: modules. Not controllers or services, but whole modules) right off the bat and if you try to declare and inject a new module after the run block, Angular will throw a tantrum.

Next, we add a resolve statement. This guarantees the route will be resolved only when the promise inside returns:

resolve: ['$q', '$ocLazyLoad', function($q, $ocLazyLoad) {
   var deferred = $q.defer();

   // This is webpack's magic. When webpack sees this require.ensure block, 
   // it automatically creates a chunk file
   // consisting of only that module and its dependencies, 
   // and it automatically takes care of loading it for you when you need it
   require.ensure([], function() {
      var mod = require('./captain-list');

      // after the chunk above loads, we tell ocLazyLoad to inject the module into our Angular app
      $ocLazyLoad.load({
         name: 'cl3.captainList',
      });

      // you can resolve it with anything
      deferred.resolve(mod.controller);
   });

   return deferred.promise;
}]

I want to emphasize again just how cool it is — whenever webpack sees a require.ensure block, it will automatically create a separate file, e.g. chunk1.bundle.js, and then load it automatically when you need it. Isn’t that mindblowingly awesome??!

One gotcha — if you do use this method, know that ng-cache loader will no longer work with your lazy-loaded components because the loader kicks in on the run block of the app. Instead, you’ll have to require in your templates using the regular html loader inside your modules:

require('html!./whatever-my-component-is');

You can experiment around with different combinations, for example, instead of using templateProvider, you could decide to bundle all your templates into the main “common” bundle to save on extra HTTP requests, e.g.:

template: require('html!./captain-list/captainList.html')

Know that there are tradeoffs, and lazy loading might not necessarily be performance-boosting. If you lazy load your modules, your users have to wait longer between page navigation. The more HTTP requests you make, the more you load your server and incur latency.

It depends on your app, your server, your users, etc. In my case, I found that, with a 310kb (total, including vendor files) bundle, perceived performance was much greater when I did not lazy load. That probably will change as app size grows.

Angular, webpack and gulp for an SPA: Part III

One thought on “Angular, webpack and gulp for an SPA: Part III

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s