How to use the RequireJS optimizer with jQuery loaded from CDN and plugins as shims

by Matt Perdeck 17. July 2012 09:44

Describes how to use the RequireJS optimizer with code that loads jQuery from a CDN and uses shims for jQuery plugins, avoiding the need to make the plugins into AMD modules.

Contents

Introduction

It is now very common for web sites to load jQuery from a CDN and to also use a number of jQuery plugins. Because most plugins are not available from a CDN, they need to be stored on the web server.

It would be great to use RequireJS to manage the dependencies of your code to the plugins and jQuery, so the plugins and jQuery are loaded automatically when your code loads. Because jQuery plugins are not AMD (Asynchronous Module Definition) modules, they need to be configured in RequireJS as shims.

However, the RequireJS documentation for shims points out that you can't use the RequireJS optimizer with code that uses both shims and libraries loaded from a CDN. This turns out to be true, but only to a certain extent. This article shows how to get around this limitation by loading your AMD modules code and your (non-AMD) plugins in separate stages.

Version 0 - simple site without RequireJS

To investigate this issue, I created a simple single page site without RequireJS at first, with a main.js file, two more custom JavaScript files and two jQuery plugins, Splatter and Toastmessage (version 0 in the download).

Here is the code:

html (note the long list of script tags!):

<h2>Index Page</h2>

<p>
<button onclick="model.save_click()">Save</button>
<button onclick="model.splat_click()">Splat!</button>
</p>

<div id="splatters"></div>

<script src="Scripts/dataAccess.js" type="text/javascript"></script>
<script src="Scripts/ticket.js" type="text/javascript"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" 
           type="text/javascript"></script>
<script src="Scripts/lib/jquery.splatter.js" type="text/javascript"></script>
<script src="Scripts/lib/jquery.toastmessage.js" type="text/javascript"></script>
<script src="Scripts/main.js" type="text/javascript"></script>

main.js:

function TicketsViewModel() {
    this.save_click = function () {
        var ticket = new Ticket("Economy", 199.95);
        var message = ticket.save();
        $().toastmessage('showNoticeToast', message);
    }

    this.splat_click = function () {
        $('#splatters').splatter({
            height: 250,
            width: 700,
            splat_count: 120
        });
    }
}

var model = new TicketsViewModel();

ticket.js:

var Ticket = function (name, price) {
    this.name = name;
    this.price = price;
    this.save = function () {
        return DataAccess.save(this.name + ' at ' + this.price);
    };
};

dataAccess.js:

var DataAccess = (function () {
    var my = {};
    my.save = function (data) {
        //TODO: Save data
        return('Saving: ' + data);
    };

    return my;
} ());

Next step is to start using RequireJS for dependency management.

Version 1 - first unsuccessful attempt

First lets make the two custom JavaScript files into AMD modules (details).

ticket.js:

define(['dataAccess'], function (DataAccess) { return (function (name, price) {
        this.name = name;
        this.price = price;
        this.save = function () {
            return DataAccess.save(this.name + ' at ' + this.price);
        };
    }); });

dataAccess.js:

define({
        'save': function (data) {
            //TODO: Save data
            return('Saving: ' + data);
        }
});

We'll make main.js into an AMD module as well (red code). Additionally (green code), we'll introduce shims for the jQuery plugins. Finally, we'll point RequireJS at the download path for jQuery.

Note that the plugins and the backup file for jQuery all live in the lib subdirectory. Also, RequireJS wants you to leave off the .js extension.

requirejs.config({ paths: { jquery: [ 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min', // If the CDN location fails 'lib/jquery-1.7.2.min' ] }, shim: { 'lib/jquery.splatter': ['jquery'], 'lib/jquery.toastmessage': ['jquery'] } }); // ----------------------------------------

require(['ticket', 'jquery', 'lib/jquery.splatter', 'lib/jquery.toastmessage'], function (Ticket, $) {
	function TicketsViewModel() {
		this.save_click = function () {
			var ticket = new Ticket("Economy", 199.95);
			var message = ticket.save();
			$().toastmessage('showNoticeToast', message);
		}

		this.splat_click = function () {
			$('#splatters').splatter({
				height: 250,
				width: 700,
				splat_count: 120
			});
		}
	}

	model = new TicketsViewModel();
});

Finally, we'll replace the long list of script tags with a single load of the require library from CDNJS:

<h2>Index Page</h2>

<p>
<button onclick="model.save_click()">Save</button>
<button onclick="model.splat_click()">Splat!</button>
</p>

<div id="splatters"></div>

<script data-main="Scripts/main" src="http://cdnjs.cloudflare.com/ajax/libs/require.js/2.0.2/require.min.js" type="text/javascript"></script>

Fails when optimized

When you run this, you'll find that this works well. However, when you then optimize the code and run the result, you'll find that the jQuery plugins get loaded before jQuery itself, causing a JavaScript error.

This is because the plugins have been combined with all other code into main.js - except for jQuery, which is loaded from the CDN. Because the plugins are not AMD modules and so have not been marked as dependent on jQuery, they get loaded before jQuery itself has been loaded.

There are two solutions to this:

  1. Make the plugins into AMD modules and make them dependent on jQuery. This way, their code will only be executed after jQuery has loaded. However, this had the disadvantage that you (and your team members) have to remember to convert new plugins and new versions of plugins whenever they are introduced to the site.
  2. Create a new AMD module that loads the plugins. By making that module dependent on jQuery, it will only load the plugins after jQuery has loaded. This way, you don't have to convert the plugins, but there will be at least one additional download.

I chose the second option because it makes maintenance a bit easier. Note that your site might be loading a lot more stuff besides JavaScript files, such as stylesheets and images. This will reduce the impact of the additional file load.

Version 2 - using a new AMD module that loads the plugins

Lets first create the new AMD module. This has a dependency on jQuery, so won't be executed until jQuery has loaded. It then calls require with the two plugins as dependencies. That will prompt RequireJS to load the plugins.

jQueryPlugins.js:

define(['jquery'], function ($) {
	// Only once jquery has loaded, load the jQuery plugins
	require(['lib/jquery.splatter', 'lib/jquery.toastmessage'], function () {
		// Do nothing
	});
});

Now change main.js, to replace the dependency on the two plugins with a dependency on jQueryPlugins

...
require(['ticket', 'jquery', 'jQueryPlugins'], function (Ticket, $) {
...

RequireJS tends to be vigorous when it comes to finding modules to combine into a single file. We need to tell RequireJS not to combine the plugins into main.js, by excluding them in the app.build.js file which controls the optimization process.

app.build.js:

({
    appDir: "../",
    baseUrl: "Scripts",
    dir: "../../RequireJSTrial.Site-build",
    modules: [
        {
            name: "main", exclude: [ 'lib/jquery.splatter', 'lib/jquery.toastmessage' ]
        }
    ],
    paths: {
        jquery: "empty:"
    }
})

Better, but not good enough

If you now optimize the code and run it, you'll find that it now runs well (see the RequireJSTrial.Site-build directory in version 2 in the downloads).

However, looking at a waterfall chart of the page load, it is clear that the plugins are loaded separatedly. Which is no wonder, seeing that we went out of our way to stop them from being combined into main.js.

Version 2 waterfall

The solution is to another AMD module which is dependent on the jQuery plugins. By listing it as a module in app.build.js, that module and the plugins will be combined into one file. We can then get jQueryPlugins.js to load this one file, instead of the individual plugins.

This all leads to ...

Version 3 - using a second AMD module to load all plugins

First we need the second module that loads all plugins.

jQueryPluginsCollection.js:

define(['lib/jquery.splatter', 'lib/jquery.toastmessage'], function ($) {
	// Do nothing
});

Update jQueryPlugins.js so it loads jQueryPluginsCollection.js instead of loading the plugins individually.

jQueryPlugins.js:

define(['jquery'], function ($) {
	// Only once jquery has loaded, load the jQuery plugins
	require(['jQueryPluginsCollection'], function () {
		// Do nothing
	});
});

Finally, list jQueryPluginsCollection.js as a module in app.build.js. That way, the optimizer will combine and minify jQueryPluginsCollection.js and its dependencies (being the plugins). We also need to tell the optimizer not to combine it with main.js.

app.build.js:

({
    appDir: "../",
    baseUrl: "Scripts",
    dir: "../../RequireJSTrial.Site-build",
    modules: [
        {
            name: "main",
			// Exclude these, otherwise they get combined into main.js, 
            // meaning they'll be loaded before jQuery comes in from 
            // the CDN.
			exclude: [
				'jQueryPluginsCollection',
				'lib/jquery.splatter',
				'lib/jquery.toastmessage'
            ]
        }, { name: "jQueryPluginsCollection" }
    ],
    paths: {
        jquery: "empty:"
    }
})

If you now run the optimizer and load the page, you'll see this waterfall. This shows how all plugins are loaded in one go after jQuery has loaded:

Version 3 waterfall

Pingbacks and trackbacks (1)+

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

Books

Book: ASP.NET Site Performance Secrets

ASP.NET Site Performance Secrets

By Matt Perdeck

Details and Purchase

About Matt Perdeck

Matt Perdeck Presenting

Matt has written extensively on ways to improve web site performance.

more >>