Saving Bandwidth With Moment Locales in the Umbraco Backoffice

Here at Offroadcode we spend a lot of time thinking about the experience of our clients' editors in the Umbraco back office. It's a philosophy that guides how we design the back office UI for humans with property editors and docTypes. It also has had us looking at ways that we can contribute back to the Umbraco community by embracing the open source ethos of the Umbraco CMS and spending some time tuning up the back office's code itself.

Pete talked about his overall vision of improving the back office in a recent blog post. In pursuit of being a small part of helping that, I submitted a recent pull request as a solution for issue U4-1104 which can be summarized as "Moment.js comes in really heavy in the back office, can we make it lighter?"

The answer, as my pull request goes to show, is we can make it a lot lighter. Almost 95% lighter.

What follows is a detailed look at the problem as I saw it, and the solution I came up with.

The Problem

The Umbraco back office uses Moment, which is a great library for dates and times in JavaScript. But it loads Moment into the app in perhaps the most inefficent fashion possible. How bad is it?

  1. It loads the unminified script with all locales: moment-with-locales.js, which is pretty beefy at 362 KB.
  2. If that weren't enough, it loads it twice.

As a result, the file size for the first load is 362 KB x 2 = 724 KB, as you can see here:

moment-original

The Original Code

The moment JS files are in the proper place in the project due to the "sources":{} object at line 45 in src/Umbraco.Web.UI.Client/bower.json:

"moment": "bower_components/moment/min/moment-with-locales.js",

The load occurs on line 157 on /src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js:

angular.module("umbraco")
    .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController)
    .run(function(mediaHelper, umbRequestHelper, assetsService){
        if (mediaHelper && mediaHelper.registerFileResolver) {
            // Right here!
            assetsService.load(["lib/moment/moment-with-locales.js"]).then(
                function () {
                    // snip... don't need to see the code in here to understand what's going on.
            );
        }
    });

The code is also loaded on line 6 on /src/Umbraco.Web/UI/JavaScript/JsInitialize.js:

'lib/moment/moment-with-locales.js',

Solution #1: Load the Minified Version

Swap to the minimized Moment with Locales script.

The Changes

  1. Modify src/Umbraco.Web.UI.Client/bower.json at line 45:
"moment": [
  "bower_components/moment/min/moment-with-locales.js",
  "bower_components/moment/min/moment-with-locales.min.js"
],
  1. Modify line 157 of /src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js:
assetsService.load(["lib/moment/moment-with-locales.min.js"]).then(
  1. Modify line 6 of /src/Umbraco.Web/UI/JavaScript/JsInitialize.js:
'lib/moment/moment-with-locales.min.js',

The Result

The file load size for first load becomes 167 KB x 2 = 334 KB.

The Savings: 390 KB smaller! 53.9% less!

moment-minimized

Solution #2: Load Moment Only Once

That's already an improvement, but we're still loading the script twice. Let's remove one of those scripts.

The Changes

  1. Remove the assetsService.load(["lib/moment/moment-with-locales.min.js"].then() from line 157 on /src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js, leaving only the interior code of:
	mediaHelper.registerFileResolver("Umbraco.UploadField", function(property, entity, thumbnail){
		if (thumbnail) {
			if (mediaHelper.detectIfImageByExtension(property.value)) {
				//get default big thumbnail from image processor
				var thumbnailUrl = property.value + "?rnd=" + moment(entity.updateDate).format("YYYYMMDDHHmmss") + "&width=500&animationprocessmode=first";
				return thumbnailUrl;
			}
			else {
				return null;
			}
		}
		else {
			return property.value;
		}
	});

The Result

The file load size for first load becomes 167 KB. There's no loss of functionality from file uploads.

The Savings: 557 KB smaller! That's 76.9% less!

moment-minimized-single

Solution #3: Only Load the Locale You Need

We're still loading all of the locales, when our user only needs one at any given time. Can we do something to shrink it more?

Yes, we can! Moment provides a large list of locale files, which exist in the Umbraco installation. Using LazyLoad, we can load the one locale we need, if we have a way of knowing what culture the user has and had a place to know that they're always entering the app.

The Changes

At line 18 of /src/Umbraco.Web.UI.Client/src/init.js we have a listener paying attention for the app.authenticated broadcast, and loading assets for the user and triggering tours after they've authenticated:

/** Listens for authentication and checks if our required assets are loaded, if/once they are we'll broadcast a ready event */
eventsService.on("app.authenticated", function(evt, data) {

    assetsService._loadInitAssets().then(function() {

	//Register all of the tours on the server
	tourService.registerAllTours().then(function () {
	    appReady(data);

	    // Auto start intro tour
	    tourService.getTourByAlias("umbIntroIntroduction").then(function (introTour) {
		// start intro tour if it hasn't been completed or disabled
		if (introTour && introTour.disabled !== true && introTour.completed !== true) {
		    tourService.startTour(introTour);
		}
	    });

	}, function(){
	    appReady(data);
	});

    });

});

Let's put a function in here to load the Moment locale we need. But because this is centered around the user, let's make that function in /src/Umbraco.Web.UI.Client/src/common/services/user.service.js to keep it in the right code bucket. We'll call the function loadMomentLocaleForCurrentUser(), and make it look like this:

/** Loads the Moment.js Locale for the current user. */
loadMomentLocaleForCurrentUser: function () {
	var deferred = $q.defer();

	var supportedLocales = [];

	this.getCurrentUser()
	    .then(function (user) {
		var locale = user.locale.toLowerCase();
		if (locale !== 'en-us') {
		    var localeUrls = ['lib/moment/' + locale + '.js'];
		    if (locale.indexOf('-') > -1) {
			localeUrls.push('lib/moment/' + locale.split('-')[0] + '.js')
		    }
		    assetsService.load(localeUrls).then(function() {
			deferred.resolve(localeUrls);
		    });
		} else {
		    deferred.resolve(['']);
		}
	    });
	return deferred.promise;
}

Some notes on what's going on here.

Now that we've added this, we'll modify our code in init.js as follows:

/** Listens for authentication and checks if our required assets are loaded, if/once they are we'll broadcast a ready event */
eventsService.on("app.authenticated", function(evt, data) {
	
	assetsService._loadInitAssets().then(function() {

		// THIS IS OUR CHANGE: Loads the user's locale settings for Moment.
		userService.loadMomentLocaleForCurrentUser().then(function() {

			//Register all of the tours on the server
			tourService.registerAllTours().then(function () {
				appReady(data);
				
				// Auto start intro tour
				tourService.getTourByAlias("umbIntroIntroduction").then(function (introTour) {
					// start intro tour if it hasn't been completed or disabled
					if (introTour && introTour.disabled !== true && introTour.completed !== true) {
						tourService.startTour(introTour);
					}
				});

			}, function(){
				appReady(data);
			});
		});
	});

});

Now that we know we can load a locale file on user login, we can ditch the moment-with-locale.min.js file for the non-locale file in /src/Umbraco.Web/UI/JavaScript/JsInitialize.js by replacing:

'lib/moment/moment-with-locales.min.js',

with:

'lib/moment/moment.min.js',

We also need to modify src/Umbraco.Web.UI.Client/bower.json to load in the moment.min.js and locale files like:

"sources": {
	"moment": [
	  "bower_components/moment/min/moment.min.js",
	  "bower_components/moment/min/moment-with-locales.js",
	  "bower_components/moment/locale/*.js"
	],
	// snip for brevity
}

(I'm keeping moment-with-locales.js for purposes of loading inside karma tests.)

The Results

For US English users we're loading a mere 34.9 KB. If we were Danish, the locale file is only another 2.3 KB. That's less than 38 KB total!

The Savings: 686 KB smaller! That means we're loading 94.7% less code!

moment-without-locale moment-locale-resource

The Conclusion

We've removed around 686 KB of assets from the initial load of the Umbraco back office with a little cleanup and extra work for being more selective in bringing in Moment locales.

If you're interested in joining us and going from being an Umbraco developer to an Umbraco contributor, you can check out this list of issues that Pete Duncanson brought up for helping out.

Love this? Give it a tweet!

Keep up to date with our posts via our RSS feed

Drop us a line

Got a project in mind or just want to say hello? Send us a mail right here.