SoFunction
Updated on 2025-03-10

Webpack implements lazy loading of AngularJS

As your single page app expands, its download time is getting longer and longer. This will not be beneficial to improving the user experience (tip: But user experience is exactly why we develop single-page applications). More code means larger files until code compression can no longer meet your needs. The only thing you can do for your user is not to let him download the entire application at once. At this time, delayed loading comes in handy. Unlike downloading all files at once, letting the user download only the files he needs now.

so. How to make your application lazy loading? It basically divides into two things. Split your module into small chunks and implement some mechanisms that allow these chunks to be loaded on demand. It sounds like there is a lot of workload, doesn't it? If you use Webpack, that won't be the case. It supports code segmentation features out of the box. In this post I assume you are familiar with Webpack, but here is an introduction if you don't know. For long story short, we will also use AngularUI Router and ocLazyLoad .

The code can be found on GitHub. You can fork it anytime.

Webpack configuration

Nothing special, really. Actually copying and pasting from the documentation directly, the only difference is that we adopt ng-annotate to keep our code concise, and adopting babel to use some magical features of ECMAScript 2015. If you are interested in ES6, you can check out this previous post. While these things are great, none of them are necessary to implement lazy loading.

// 
var config = {
entry: {
app: ['./src/core/'],
},
output: {
path: __dirname + '/build/',
filename: '',
},
resolve: {
root: __dirname + '/src/',
},
module: {
noParse: [],
loaders: [
{ test: /\.js$/, exclude: /node_modules/,
loader: 'ng-annotate!babel' },
{ test: /\.html$/, loader: 'raw' },
]
}
};
 = config;

application

The application module is the main file and it must be included, which requires a forced download on every page. As you can see, we won't load anything complicated except global dependencies. Unlike loading controllers, we only load the routing configuration.

// 
'use strict';
export default require('angular')
.module('lazyApp', [
require('angular-ui-router'),
require('oclazyload'),
require('./pages/home/').name,
require('./pages/messages/').name,
]);

Routing configuration

All lazy loading is implemented in the routing configuration. As I said, we are using AngularUI Router because we need to implement nested views. We have several use cases. We can load the entire module (including child state controllers) or load a controller for each state (not considering dependencies on parent state).

Load the entire module

When the user enters the /home path, the browser will download the home module. It includes two controllers for home and these two states. We can achieve lazy loading through the resolve attribute in the configuration object of state. Thanks to Webpack's approach, we can create the home module into the first code block. It is called . Without $ , we will find an error Argument 'HomeController' is not a function, got undefined , because in Angular's design, the way to load the file after starting the application is not feasible. But $ allows us to register a module during the startup phase and then use it after it is loaded.

// 
'use strict';
function homeRouting($urlRouterProvider, $stateProvider) {
$('/home');
$stateProvider
.state('home', {
url: '/home',
template: require('./views/'),
controller: 'HomeController as vm',
resolve: {
loadHomeController: ($q, $ocLazyLoad) => {
return $q((resolve) => {
([], () => {
// load whole module
let module = require('./home');
$({name: 'home'});
resolve();
});
});
}
}
}).state('', {
url: '/about',
template: require('./views/'),
controller: 'HomeAboutController as vm',
});
}
export default angular
.module('', [])
.config(homeRouting);

The controller is regarded as a dependency of the module.

// 
'use strict';
export default angular
.module('home', [
require('./controllers/').name,
require('./controllers/').name
]);

Load the controller only

What we do is the first step forward, so let's move on to the next step. This time, there will be no large modules, only a streamlined controller.

// 
'use strict';
function messagesRouting($stateProvider) {
$stateProvider
.state('messages', {
url: '/messages',
template: require('./views/'),
controller: 'MessagesController as vm',
resolve: {
loadMessagesController: ($q, $ocLazyLoad) => {
return $q((resolve) => {
([], () => {
// load only controller module
let module = require('./controllers/');
$({name: });
resolve();
})
});
}
}
}).state('', {
url: '/all',
template: require('./views/'),
controller: 'MessagesAllController as vm',
resolve: {
loadMessagesAllController: ($q, $ocLazyLoad) => {
return $q((resolve) => {
([], () => {
// load only controller module
let module = require('./controllers/');
$({name: });
resolve();
})
});
}
}
})

I believe there is nothing special here, the rules can be left unchanged.

Loading Views

Now, let's temporarily let the controller go and focus on the view. As you may have noticed, we embed the view into the routing configuration. If we didn't put all the routing configurations in it, this wouldn't be a problem, but now we need to do that. This case is not about delaying the loading of the routing configuration but rather the view, so it will be very simple when we use Webpack to implement it.

// 
...
.state('', {
url: '/new',
templateProvider: ($q) => {
return $q((resolve) => {
// lazy load the view
([], () => resolve(require('./views/')));
});
},
controller: 'MessagesNewController as vm',
resolve: {
loadMessagesNewController: ($q, $ocLazyLoad) => {
return $q((resolve) => {
([], () => {
// load only controller module
let module = require('./controllers/');
$({name: });
resolve();
})
});
}
}
});
}
export default angular
.module('', [])
.config(messagesRouting);

Beware of repeated dependencies

Let's take a look at the contents of .

// 
'use strict';
class MessagesAllController {
constructor(msgStore) {
 = ();
}
}
export default angular
.module('', [
require('commons/msg-store').name,
])
.controller('MessagesAllController', MessagesAllController);
// 
'use strict';
class MessagesNewController {
constructor(msgStore) {
 = '';
this._msgStore = msgStore;
}
create() {
this._msgStore.add();
 = '';
}
}
export default angular
.module('', [
require('commons/msg-store').name,
])
.controller('MessagesNewController', MessagesNewController);

The root of our problem is require('commons/msg-store').name . It requires msgStore, a service, to realize message sharing between controllers. This service exists in both packages. There is one in , and there is another in . Now, it has no room for optimization. How to solve it? Just add msgStore as a dependency for the application module. While this is not perfect enough, in most cases, it's enough.

// 
'use strict';
export default require('angular')
.module('lazyApp', [
require('angular-ui-router'),
require('oclazyload'),
// msgStore as global dependency
require('commons/msg-store').name,
require('./pages/home/').name,
require('./pages/messages/').name,
]);

Tips for unit testing

Changing msgStore to be a global dependency does not mean that you should remove it from the controller. If you do this, when you write the tests, without mocking this dependency, it won't work properly. Because in unit tests, you will only load this one controller instead of the entire application module.

// 
'use strict';
describe('MessagesAllController', () => {
var controller,
msgStoreMock;
beforeEach((require('./').name));
beforeEach(inject(($controller) => {
msgStoreMock = require('commons/msg-store/');
spyOn(msgStoreMock, 'all').(['foo', ]);
controller = $controller('MessagesAllController', { msgStore: msgStoreMock });
}));
it('saves () in msgs', () => {
expect().toHaveBeenCalled();
expect().toEqual(['foo', ]);
});
});

The above content is the lazy loading of AngularJS that the editor shared with you. I hope it will be helpful to everyone!