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?
It loads the unminified script with all locales: moment-with-locales.js, which is pretty beefy at 362 KB.
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:
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
- 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"
],
- 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(
- 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!
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
- 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!
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 servertourService.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](https://user-images.githubusercontent.com/4752923/37106800-907e900e-21e7-11e8-9679-653131811cf6.PNG) ![moment-locale-resource](https://user-images.githubusercontent.com/4752923/37106870-ae79ba8e-21e7-11e8-8408-0da613e9b517.PNG)
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.