Load faster: Convert your existing yeoman based AngularJS 1.x application to lazy load
In this blog entry I have attempted to show you how to convert AngularJS yeoman based Single Page Application (SPA) to lazy load or download file on demand with minor modifications to code.
(https://github.com/yeoman/generator-angular)
Why I chose to write?
While working on a SaaS app bookingjoe.com, I was faced with a challenge where the first page, which included large minified JS file, was taking time to load. I tried out several approaches.
- AngularJS with RequireJS: There are many generators available on the web to use RequireJS with AngularJS but they do not solve issue of on-demand loading & downloading of AngularJS modules and components registering problem. AngularJS does not allow registration of modules and components after initial bootstrapping of angular app and so RequireJS could only load javascript files but it could not register new angular modules and components on demand. Also after using RequireJS, the main.js file grows to ~500kb. This didn’t work.
- AngularJS with ocLazyLoad (https://github.com/ocombe/ocLazyLoad): My requirement was to load first page quickly and load the remaining JS files on demand. I found OCLazyLoad bower component was addressing this problem. It was also light weight. OCLazyLoad registers the modules and components with AngularJS dynamically.
I was still mulling over it but was finally convinced when I read the following thread on stack overflow:
http://stackoverflow.com/questions/28222096/angularjs-oclazyload-vs-requirejs
Hat tip to Olive for pointing me in the right direction. Armed with this knowledge, I experimented converting Yeoman based AngularJS project to lazy load it’s components. Since my problem was conversion of existing application to lazy load it’s modules, I created a fresh AngularJS project using Yeoman generator and applied my changes to the generated file.
What I did…
- Added a reference of ocLayzLoad and updated JS files reference to load on demand from app.js or .html file of their views.
- Updated Gruntfile to uglyfy, renamed file name and updated references in the final .html or .js file.
So let’s see how I did it…
- Created a new Yeoman AngularJS project
1234npm install -g grunt-cli bower yo generator-karma generator-angularmkdir HelloWorldcd HelloWorldyo angular HelloWorld
- Added ocLazyLoad and update references of JS files into index.html.
First install the ocLazyLoad bower component1bower install oclazyload --save-devNow load the module ‘oc.lazyLoad’ in application. Update app.js file.
12345678angular.module('helloWorldApp', [fusion_builder_container hundred_percent="yes" overflow="visible"][fusion_builder_row][fusion_builder_column type="1_1" background_position="left top" background_color="" border_size="" border_color="" border_style="solid" spacing="yes" background_image="" background_repeat="no-repeat" padding="" margin_top="0px" margin_bottom="0px" class="" id="" animation_type="" animation_speed="0.3" animation_direction="left" hide_on_mobile="no" center_content="no" min_height="none"]['ngCookies','ngResource','ngRoute','ngSanitize','oc.lazyLoad',])To load controller and services js files dynamically, remove their references from index.html. I also needed to update app.js file to remove controller’s name from route entry. Now my app.js route looked like
12345678910$routeProvider.when('/about', {templateUrl: 'views/about.html'}).when('/contact', {templateUrl: 'views/contact.html'}).otherwise({redirectTo: '/'});Now there are two ways to load controllers and services dynamically.
- Load JS file by adding reference of JS in .html file
I added controller and services reference in html view. Here I added required controllers and services js details in about.html. I also provided about.js and helloservie.js file path in html tag “oc-lazy-load”.12345<div oc-lazy-load="['scripts/controllers/about.js', 'scripts/services/helloservice.js']"><div ng-controller="AboutCtrl as about">Your html goes here</div></div> - Reference of JS in app.js file(contact.html, app.js)
With $ocLazyLoadProvider (app.js), I created list of modules in app.js and specified associated list of files to be loaded with each module. These module definitions do not download file during declaration. They will be downloaded with view loading.1234567$ocLazyLoadProvider.config({modules: [{name: 'helloWorldApp.contact',files: ['scripts/controllers/contact.js','scripts/services/contactservice.js']}]});12345<div oc-lazy-load="helloWorldApp.contact"><div ng-controller="ContactCtrl as contact">Your html goes here</div></div>
- Load JS file by adding reference of JS in .html file
- Updated Gruntfile.js to Copy and Uglyfy JS files in final build
To avoid loading of controllers and services JS with first page load, I removed references of those JS from index.html. This will create another problem in build. The final build will not have those JS files. I needed to update COPY block in Gruntfile.js to copy remaining controllers and services JS.12345678910111213141516171819202122232425262728293031323334copy: {dist: {files: [{expand: true,dot: true,cwd: '<%= yeoman.app %>',dest: '<%= yeoman.dist %>',src: ['*.{ico,png,txt}','*.html','scripts/*/*.js', //Added to copy various folder's JS file to dist.'views/{,*/}*.html','images/{,*/}*.{webp}','styles/fonts/{,*/}*.*']}, {expand: true,cwd: '.tmp/images',dest: '<%= yeoman.dist %>/images',src: ['generated/*']}, {expand: true,cwd: 'bower_components/bootstrap/dist',src: 'fonts/*',dest: '<%= yeoman.dist %>'}]},styles: {expand: true,cwd: '<%= yeoman.app %>/styles',dest: '.tmp/styles/',src: '{,*/}*.css'}}Module “filerev” changes JS file name based on hash value(shasum) of file content. When JS files are part of index.html, this operation is done on final minified and concatenated JS file. But here JS files are referred from different JS (app.js) and HTML files. To solve this problem, I updated “usemin” section of Grunt.
12345678910111213141516171819usemin: {html: ['<%= yeoman.dist %>/{,*/}*.html'],css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],js: ['<%= yeoman.dist %>/scripts/{,*/}*.js'],options: {assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images','<%= yeoman.dist %>/styles'],patterns: {js: [[/(images\/[^''""]*\.(png|jpg|jpeg|gif|webp|svg))/g, 'Replacing references to images'],[/(scripts\/[^''""]*\.(js))/g, 'Replacing references to javascript'] //Scan js files to update reference],html: [[/(scripts\/[^''""]*\.(js))/g, 'Replacing references to javascript']] //Scan html files to update reference}}}Final step in Gruntfile.js is minification and uglification. I updated uglify section (which was commented in original project) to uglify separate JS files.
12345678910uglify: {dist: {files: [{expand: true,cwd: '<%= yeoman.dist %>/scripts',src: '*/*.js',dest: '<%= yeoman.dist %>/scripts'}]}}Quick tip: Uglification changes AngularJS variables and injects service variable names. So here one has to map the injection of services to it’s variable manually.
1234angular.module('helloWorldApp').controller('AboutCtrl',['helloService', function (helloService) {this.helloMessage = helloService.SayHello();}])
Conclusion
There still remained some challenges with ocLazyLoad reference resolutions. I had to solve those issues individually. I am now convinced many Single Page Applications (SPA) can benefit from this enhancement. Would welcome your queries and suggestions. I will be happy to respond.
Source Code https://github.com/dhirenrouterabbit/angular-yeoman-oclazyload[/fusion_builder_column][/fusion_builder_row][/fusion_builder_container]