Angular Material Design custom build: include only the parts you need

I reduced the file size of my app’s dependencies by 15% by only including the Angular Material source code my app actually uses. This simple action improved page load speed and this should reduce bounce rate. As the app in question is another app’s marketing app/landing page, this improvement can have real monetary value.

The landing page is built with Angular Material (because so is the app it advertises). Angular Material is heading for feature parity with the components detailed in the Material Design spec. These features are awesome, but the landing page doesn’t use them all.

For the landing page, these unused features are wasteful: user wait for bloat to download that the app never uses. If we strip away unused features from our app then we can make the app load a little bit faster.

This post will explain how to easily make a custom Angular Material build using Gulp that includes only the needed features, without having to pull the Angular Material repo.

Angular Material Design “modules”

The Angular Material bower package comes with a “/modules/js/” folder, which has each of the components split into different source folders:

autocomplete
backdrop
bottomSheet
...
whiteframe

This makes it super easy for us to build only the components we need. We will need to do a bit of boilerplate to define the angular module that these components will live under.

Gulp

My build tasks looks something like below. When “buildDependencies” task is ran, it first finishes “buildMaterial” task, which creates “custom.js” in the angular material folder. “buildDependencies” task  then picks up custom.js and concatenate it with the other dependencies.

var insert = require('gulp-insert');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var DIR_JS_LIBS = './assets/js/libs';
var DIR_BUILD = './build/'
gulp.task('buildMaterial', function () {
    var modules = [
        'angular-material/modules/js/core/core.js',
        'angular-material/modules/js/core/default-theme.js',
        'angular-material/modules/js/backdrop/backdrop.js',
        'angular-material/modules/js/toast/toast.js',
        'angular-material/modules/js/sticky/sticky.js',
    ];
    var boilerplate = [
        '(function(){' +
        '    angular.module("ngMaterial", ["ng", "ngAnimate", "ngAria", "material.core", "material.core.theming.palette", "material.core.theming", "material.components.toast"]);' +
        '    })();'
    return gulp.src(modules, {cwd: DIR_JS_LIBS})
        .pipe(concat({path: 'custom.js', cwd: ''}))
        .pipe(insert.prepend(boilerplate))
        .pipe(gulp.dest(DIR_JS_LIBS + 'angular-material/'));
});

gulp.task('buildDependencies', ['buildMaterial'], function () {
    var deps = [
        'angular/angular.js',
        'angular-route/angular-route.js',
        'angular-ui-router/release/angular-ui-router.js',
        'angular-material/custom.js',
    ];
    return gulp.src(deps, {cwd: DIR_JS_LIBS})
        .pipe(concat({path: 'deps.min.js', cwd: ''}))
        .pipe(uglify())
        .pipe(gulp.dest(DIR_BUILD));
});

Outcome

The above example is a stripped down version of the real task. In reality the project uses about 7 components. After minifying I saw I saved about 60kb by using the custom build. This is approximately a 15% reduction.

This is not a huge saving, but it is worth the effort if continual improvement philosophy is followed then many small improvements can lead to great savings.

FWIW, click here to see my project using the custom Angular Material build. Its a marketing app for “Monofox Ask Peers” – basically a slimmed down private stack overflow for schools.

Memoize AJAX requests without data inconsistency

A common problem working with ajax is firing duplicate requests. A common solution is to first check if a request is ongoing. I solved this problem differently by memoizing the get request for the lifetime of the request.

Memoization is storing the results of a function call and returning the cached result when the same inputs occur. This is often used to optimize expensive function calls, but I see value in using it here. In the code below we use Restangular, but the concept works just as well for any promise based request library.

function memoizedService(route) {
    var service = Restangular.service(route);
    var hasher = JSON.stringify;
    var memoizedGetList = _.memoize(service.getList, hasher);
 
    /**
    * Get list, but if there already is a request ongoing with the same
    * params then return the existing request.
    * @param {Object} params
    * @return {promise}
    */

    service.getList = function(params) {
        return memoizedGetList(params).finally(function() {
            delete memoized.cache[hasher(params)];
        });
    }

     return service;
}
var questionEndpoint = Restangular.memoizedService('question');

Make sure you install underscore with a bit of

bower install underscore --save

Then, to use the feature we would do:

questionEndpoint.getList({page: 1});

If we request page 1 when there is already a request for page 1 then the second `getList` will return the same promise that the first request returned and you will only see 1 request for page 1 in the network panel. Note importantly we also remove the cached request when the request is complete. This prevents data consistency problems: a get request can return different data over time (e.g., more records), and we want to make sure the user receives the most up to date data.