Lazy loading Angular 1.X states with WebPack

Lazy loading Angular 1.X states with WebPack

Our SPAs used to be bundled in one (or two) huge files. As consequence, every single part of the application gets loaded in the initial charge and, in some cases, those parts correspond to big sections that have a low usage rate (a general example of this are admin sections). This causes a long lasting initial loading time caused by the inclusion of assets that won’t even be required. Most used module bundlers, like RequireJS and Browserify, don’t come with a stock ‘module-splitting’ functionality, and that’s why WebPack called our attention. Their propaganda claim to have a solution for our concerns (https://webpack.github.io/docs/what-is-webpack.html) and that’s why we decided to give it a try.

WebPack is a module bundler and a build system altogether. Basically, WebPack creates a dependency graph by looking for the import statements in your app, using it to emit one or multiple bundles. With loaders, WebPack preprocesses different kind of files like SASS, TypeScript, images, and so forth, allowing to require them as if they were just another module. This way, WebPack deeply knows our app (specifically our dependency tree) and this allows it to make automatic optimizations to it. Inserting ‘splitting-points’ in your code (i.e. the places where your code can be splitted), tells WebPack to build your app creating ‘chunks’ that are loaded on-demand by the browser (via AJAX calls).

Additionally, WebPack has some great optimization features. For example, if more than one of your modules get bundled in different chunks, and both of them import the same external dependency, WebPack’s CommonChunksPlugin will analyze your code and place this common dependency in one separate chunk, avoiding placing the same piece of code in multiple bundles. WebPack has a lot of these optimization plugins, for both development and production environments.

Aiming to local development environment, WebPack has its own auto-reloading feature called Hot Module Replacement. As WebPack already knows the source-tree of our application, it won’t reload the whole page every time we change a module. Instead, WebPack patches them in our browser, without reloading the whole page.

A nice place to get started with WebPack is this post https://blog.madewithlove.be/post/webpack-your-bags/. It gives a broad introduction and helps to obtain an idea of the potentiality of this tool.

Lazy-loading modules in Angular 1.5 with WebPack

Next, I’ll show an example of how to load a whole UI-Router state on-demand of an Angular 1.X SPA, using OCLazyLoad and WebPack to help us achieving it. As we’ve already mentioned, we can split our app in as many bundles as we want, but Angular 1.X doesn’t natively allow us to lazily load it’s modules or components (controllers, directives, services, etc). The OCLazyLoad library brings a solution to this.

To follow this guide you will need node => v5.10.1 and gulp. We recommend to use nvm to install and manage node versions. To install gulp, first install node and then run npm install gulp -g.

Clone the NG6-Starter (https://github.com/AngularClass/NG6-starter) repository. This repo offers a project starter for scalable apps using Angular 1.5 + ES6 + WebPack.

				
					git clone https://github.com/AngularClass/NG6-starter AngularLazyLoadExample && cd AngularLazyLoadExample && npm install
				
			

Next, type npm install oclazyload --save to install the OCLazyLoad module. Our app will live in the client/ directory. Delete all the folders inside client/app/components/ and remove the client/app/common/ (it won’t be necessary for the sake of the example). You must also remove the unnecessary imports in app.js and add ocLazyLoad. It should look something like this:

				
					// app.js
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import ocLazyLoad from 'oclazyload';

import Components from './components/components';
import AppComponent from './app.component';
import 'normalize.css';

angular.module('app', [
    uiRouter,
    Components,

    ocLazyLoad
  ])
  .config(($locationProvider, $stateProvider, $urlRouterProvider) => {
    "ngInject";
    // @see: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions
    // #how-to-configure-your-server-to-work-with-html5mode
    $locationProvider.html5Mode(true).hashPrefix('!');<a id="post-preview" class="preview button" href="https://blog.octobot.io/?p=213&preview=true" target="wp-preview-213">Preview</a>
  .component('app', AppComponent);
				
			

Also delete the Home and About imports from components/components.js file.

				
					//components/components.js
import angular from 'angular';

let componentModule = angular.module('app.components', [

])

.name;

export default componentModule;
				
			

If you now run gulp serve in the console, you should see that WebPack server runs with no errors. The output should be something like this:

				
					[11:20:44] Requiring external module babel-register
[11:20:46] Using gulpfile ~/AngularLazyLoadExample/gulpfile.babel.js
[11:20:46] Starting 'serve'...
[11:20:47] Finished 'serve' after 416 ms
[BS] Access URLs:
 ------------------------------------
       Local: http://localhost:3001
    External: http://10.24.0.106:3001
 ------------------------------------
          UI: http://localhost:3002
 UI External: http://10.24.0.106:3002
 ------------------------------------
[BS] Serving files from: client
webpack built 16d62255768511c6bee1 in 6221ms
Hash: 16d62255768511c6bee1
Version: webpack 1.13.3
Time: 6221ms
               Asset     Size  Chunks             Chunk Names
       app.bundle.js  3.68 kB       0  [emitted]  app
    vendor.bundle.js  2.03 MB       1  [emitted]  vendor
   app.bundle.js.map  4.43 kB       0  [emitted]  app
vendor.bundle.js.map  2.39 MB       1  [emitted]  vendor
          index.html  1.27 kB          [emitted]
webpack: bundle is now VALID.
webpack: bundle is now INVALID.
webpack building...
webpack built 16d62255768511c6bee1 in 203ms
Hash: 16d62255768511c6bee1
Version: webpack 1.13.3
				
			

So, let’s start to create our app.

Go and generate a couple of components with the component generator offered by NG6-Started. These components will be our mini application’s states.

				
					gulp component --name nonLazy
gulp component --name lazy
				
			

After these changes, our client folder looks like this:.

				
					client
├── app
│   ├── app.component.js
│   ├── app.html
│   ├── app.js
│   ├── app.styl
│   └── components
│   ├── components.js
│   ├── lazy
│   │   ├── lazy.component.js
│   │   ├── lazy.controller.js
│   │   ├── lazy.html
│   │   ├── lazy.js
│   │   ├── lazy.spec.js
│   │   └── lazy.styl
│   └── nonLazy
│       ├── nonLazy.component.js
│       ├── nonLazy.controller.js
│       ├── nonLazy.html
│       ├── nonLazy.js
│       ├── nonLazy.spec.js
│       └── nonLazy.styl
└── index.html
				
			

We are going to add the new nonLazy component to the components/components.js module definition. It’s important to recall that the lazy component must not be imported here. If we add the import lazy from './lazy/lazy' sentence, WebPack will insert the lazy module to the main bundle.

				
					//components/components.js
import angular from 'angular';
import NonLazy from './nonLazy/nonLazy';

let componentModule = angular.module('app.components', [
  NonLazy
])

.name;

export default componentModule;
				
			

Now we should create our root state and a child that will load our nonLazy component. To do that, go to app.js and define them:

				
					import angular from 'angular';
import uiRouter from 'angular-ui-router';
import ocLazyLoad from 'ocLazyLoad';

import Components from './components/components';
import AppComponent from './app.component';
import 'normalize.css';

angular.module('app', [
  uiRouter,
  Components,
  ocLazyLoad
])
.config( ($locationProvider, $stateProvider, $urlRouterProvider) => {
  "ngInject";
  // @see: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions
  // #how-to-configure-your-server-to-work-with-html5mode
  $locationProvider.html5Mode(true).hashPrefix('!');

  $stateProvider
    .state('app', {
      abstract: true,
      template: ''
    })
    .state('app.non-lazy', {

      //
      // This is a regular, non-lazy, state definition

      url: '/non-lazy',
      template: ''
    });

  $urlRouterProvider.otherwise('/non-lazy');
})

.component('app', AppComponent);
				
			

If you go to the browser and navigate to our app (url is in the output of the gulp serve command) you should see the app running and our nonLazy component loaded as default.

If you check the server console output, you’ll see that WebPack is generating two bundles, and app.bundle.js and a vendor.bundle.js. The first one contains our whole app, and vendor.js is where all external libraries live (all the installed with npm, in our case: Angular, Angular UI Router, OCLazyLoad, etc).

Now, let’s config the lazy component to be separatedly bundled and lazy loaded.

To do so, we need to write particular code in the state definition. Let’s do that (some comments are added with brief explanations):

				
					 // app.js
    ...
    .state('app.lazy', {
      //
      // To lazy load a whole state of our app we need to add
      // a resolve method.
      url: '/lazy',
      template: '',
      resolve: {
        lazyLoad: ($q, $ocLazyLoad) => {
          "ngInject";
          let deferred = $q.defer();
          //
          // Async require => Split point
          require.ensure([], function () {
            //
            // All the code here, plus the required modules
            // will be bundled in a separate file.
            let module = require('./components/lazy/lazy');
            //
            // OCLazyLoad's 'load' function loads the Angular module.
            $ocLazyLoad.load({
              name: module.default
            });

            deferred.resolve(module);
          });
          return deferred.promise;
        }
      }
    })
    ...
				
			

The WebPack console log now shows that a new bundle, called 2.2.bundle.js, was crated.

				
					webpack building...
webpack built 5d18020a204235b2ed59 in 322ms
Hash: 5d18020a204235b2ed59
Version: webpack 1.13.3
Time: 322ms
               Asset     Size  Chunks             Chunk Names
       app.bundle.js   8.3 kB       0             app
    vendor.bundle.js  2.03 MB       1             vendor
       2.2.bundle.js   3.2 kB       2
   app.bundle.js.map  10.7 kB       0             app
vendor.bundle.js.map  2.39 MB       1             vendor
   2.2.bundle.js.map  4.28 kB       2
          index.html  1.27 kB          [emitted]
				
			

The key part here is in the lazyLoad resolve method. require.ensure is the CommonJS method to load code on demand and WebPack will take this as a split point. That means that all the code in require.ensure’s callback method will be bundled in a separate file (the one that WebPack named 2.2.bundle.js). In this particular case, the lazy module (defined in ./components/lazy/lazy.js) will live in a different bundle. To be able to lazy load the lazy Angular module, we need to use the load function of the $ocLazyLoad service.

It’s important to note that there is no difference between how we define a lazy loaded component with a non-lazy one, the only place we need to apply particular code is in the state definition.

We can create a button in the nonLazy component to navigate to the lazy one to observe the behaviour.

				
					<!-- components/nonLazy/nonLazy.html -->
<div>
<h1>{{ $ctrl.name }}</h1>

</div>
<button>Go to lazy</button></pre>
				
			

This gif animation shows how the second bundle (2.2.bundle.js) is lazy loaded when the lazy state is accessed. Once the bundle is fetched, it won’t be requested again.

The full example can be found here: https://github.com/octobot-dev/angular-webpack-lazyload-example. Feel free to clone it to check the details.

See related posts