Skip to content

Instantly share code, notes, and snippets.

@cssquirrel
Last active March 7, 2018 18:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cssquirrel/965142c8997801ca5a6759a574a7f3d5 to your computer and use it in GitHub Desktop.
Save cssquirrel/965142c8997801ca5a6759a574a7f3d5 to your computer and use it in GitHub Desktop.
Locale and Moment in the Umbraco-CMS Back Office

Locale and Moment in the Umbraco-CMS Back Office

Table of Contents
The Problem
The Original Code
Solution #1: Load the Minified Version
Solution #2: Load Moment Only Once
Solution #3: Only Load the Locale You Need
The Conclusion

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.

  • We're getting the culture of the user from userService.getCurrentUser().
  • If the culture is en-US we're not going to do anything more, as that's Moment's default locale.
  • Otherwise, we want to try to load the locale file associated with the culture. We do have a problem, though, in that the user's culture doesn't always exactly match Moment's locale file naming conventions. For example, Danish in Moment is da.js, but in Umbraco's user culture it's set as da-dk. Conversely, in some cases Moment doesn't have a non-hyphenated version of some cultures, So if the culture is hyphenated, we'll attempt to load Moment locale files for the hypenated and non-hyphenated versions of the culture.

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.

At Offroadcode we've been talking a lot about the Umbraco back office and ways the community can come help clean up some of its code with fixes like these. If you're interested in 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, as well as the blog post where he gives his initial thoughts and a call to arms in the community giving back more in this space.

If you've got thoughts on the code changes I'm proposing here, or anything related to JS or the Umbraco back office that you'd like to discuss with me, I'm @cssquirrel 🐿️ on Twitter and I always enjoy a good talk about code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment