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 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 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',
Swap to the minimized Moment with Locales script.
- 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 file load size for first load becomes 167 KB x 2 = 334 KB.
The Savings: 390 KB smaller! 53.9% less!
That's already an improvement, but we're still loading the script twice. Let's remove one of those scripts.
- 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 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!
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.
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 asda-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.)
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!
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.