Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Azeez Olaniran
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Azeez Olaniran

Lazy Loading RequireJS Modules When They Are First Requested

By
Published in Comments (8)

I've been loving RequireJS as a framework that helps me think about my JavaScript applications in a more modular fashion. However, in my research and development, I've always been loading all of my RequireJS modules upfront, at the start of the application. As I've begun to move some RequireJS functionality into production, I've found myself not wanting to eagerly load modules that hardly ever get used. Rather, I'd like to lazy load some modules if, and only if they ever get requested by the user.

To explore the lazy loading of RequireJS modules, I set up a simple test page with some content and a "help" link at the bottom. The help link at the bottom makes use of an FAQ module; however, since most people will never use this link, I don't want to load the FAQ module until it is absolutely necessary (ie. until the user clicks the link).

index.htm - Our Demo Page

<!doctype html>
<html>
<head>
	<meta charset="utf-8" />

	<title>Lazy Loading RequireJS Modules</title>

	<link rel="stylesheet" type="text/css" href="./css/demo.css"></link>

	<!-- Load RequireJS and define the bootstrap file. -->
	<script
		type="text/javascript"
		src="./js/lib/require/require.js"
		data-main="./js/main.js">
	</script>
</head>
<body>

	<h1>
		Lazy Loading RequireJS Modules
	</h1>

	<ul class="m-nav">
		<li>
			<a href="#">Home</a>
		</li>
		<li>
			<a href="#">About</a>
		</li>
		<li>
			<a href="#">Contact</a>
		</li>
	</ul>

	<p>
		Here is some content for the demo - this is just page filler.
		Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam
		sit amet volutpat sapien. Lorem ipsum dolor sit amet,
		consectetur adipiscing elit. Etiam fringilla consectetur orci.
		Cras enim lectus, mollis ac luctus sit amet, dignissim sed
		ante. Cras non erat massa, sit amet elementum sapien. Sed
		ac sapien sem. Quisque in mauris mi. Nulla pharetra accumsan
		erat. Ut sit amet eros dui. Aenean eget eros sit amet ante
		bibendum convallis at ut dui. Lorem ipsum dolor sit amet,
		consectetur adipiscing elit. Donec tristique nisl nec nibh
		faucibus tincidunt et in leo. Nunc orci est, dictum vel
		adipiscing pharetra, hendrerit et sem. Duis risus erat,
		hendrerit quis facilisis ac, feugiat eget urna.
		Pellentesque habitant morbi tristique senectus et netus
		et malesuada fames ac turpis egestas.
	</p>


	<!-- BEGIN: Lazily Loaded Module. -->
	<p class="m-help">
		Need help? <a href="#">Check out our FAQs</a>.
	</p>
	<!-- END: Lazily Loaded Module. -->

</body>
</html>

If you look at the index page, you'll see that there is a "check out our FAQs" link at the bottom. When this link is clicked, I need to load the FAQ module and then open it.

RequireJS provides the require() function which can be used to load modules on-demand. However, in order to make sure that the intermediary / loading state of the module doesn't cause unexpected behaviors (or duplicate behaviors), I have to add some logic that "debounces" clicks (in a loose sense) while RequireJS is asynchronously loading the requested module.

For this demo, I have put the lazy loading logic in the bootstrap file.

Main.js - Our Application Bootstrap

// Set up the paths for the application.
requirejs.config({
	paths: {
		"domReady": "lib/require/domReady",
		"jquery": "lib/jquery/jquery-1.7.2.min",
		"templates": "templates",
		"text": "lib/require/text",
		"views": "views"
	}
});

// Run the scripts when the DOM-READY event has fired.
require(
	[
		"jquery",
		"domReady!"
	],
	function( $ ){


		// Since the Help / FAQ module is probably going to be rarely
		// used by the user, I don't want to bother loading it as
		// part of the initial page load. As such, I'll lazy-load it
		// when the "launch" link is clicked.
		(function(){

			// Our FAQ module will start out as null until loaded.
			// And, it's not loaded until it's first needed.
			var faq = null;
			var body = $( "body" );
			var launchFaq = $( "p.m-help a" );

			// I load the FAQ module the first time it is needed.
			var handleClick = function( event ){

				event.preventDefault();

				// Check to see if the FAQ module is currently being
				// lazily loaded.
				if (faq === "loading"){

					// Ignore this click - when the module finallly
					// loads, it will open the module.
					return;

				}

				// Check to see if the module has been loaded.
				if (faq !== null){

					// Open the FAQ module for a subsequent time.
					faq.open( body );

				// The module is unloaded and unrequested. Let's load
				// it for the first time and then open it.
				} else {

					// Set an intermediary value to the faq module so
					// that subsequent requests don't try to launch
					// the module more than once.
					faq = "loading";

					// Load the FAQ module.
					require(
						[ "views/faq" ],
						function( FAQ ){

							// Create and cache an instance of the
							// FAQ module.
							faq = new FAQ();

							// Open the FAQ module for the FIRST time.
							faq.open( body );

						}
					);

				}

			};

			// Bind the click-handler for the help link.
			launchFaq.click( handleClick );

		})();


	}
);

Here, you can see that I have a click handler for the "check out our FAQs" link. In order to lazy load the FAQ module, this click handler needs to be aware of the three states of a lazy loaded module:

  1. Unloaded
  2. Loading
  3. Loaded

The first time that this link is clicked, I am using RequireJS and the require() function to load the FAQ module. This action sets the module reference to an intermediary value of "loading". This is necessary in order to debounce subsequent clicks that may occur during the loading phase. Once the FAQ module is loaded, however, any subsequent clicks will cause the initialized module to be opened.

I don't know if this is overkill. They say that premature optimization is the root of all evil; but is lazy loading rarely-used modules premature? It's definitely easier to front-load all your modules, no doubt; but, the amount of code required to lazy load the modules doesn't seem to be that overwhelming.

Anyway, this is my first pass at this sort of thing. I think I could probably factor out some of this logic and make the code smaller and more straightforward.

Also, if you are interested in the FAQ module I used in this demo, here's the class definition:

views/faq.js - FAQ Module Definition

// Define the module.
define(
	[
		"jquery",
		"text!templates/faq.htm"
	],
	function( $, faqHtml ){


		// I provide access to the FAQ modal window.
		function FAQ(){

			// Cache DOM references.
			this.dom = {};

			// When creating the detached node for the module, filter
			// out all the whitspace, text nodes, and comments that
			// come with the module.
			this.dom.target = $( faqHtml ).filter( "div.m-faq" );
			this.dom.close = this.dom.target.find( "a.close" );

			// Bind to the close link.
			this.dom.close.click( $.proxy( this, "_handleCloseClick" ) );

		}

		// Define the class methods.
		FAQ.prototype = {

			// I close the module's modal window.
			close: function(){

				// Detach the modal window - we are using detach vs.
				// remove so that we KEEP the event bindings.
				this.dom.target.detach();

			},


			// I response to internal click events on the close link.
			_handleCloseClick: function( event ){

				// Cancel the default event - not a real link.
				event.preventDefault();

				// Close the window.
				this.close();

			},


			// I open the modal window, attaching the module to the
			// given parent.
			open: function( parent ){

				parent.append( this.dom.target );

			}

		};


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Return the module constructor.
		return( FAQ );


	}
);

... and here's the template that is depends upon:

templates/faq.htm - FAQ Template

<!-- BEGIN: FAQ Module. -->
<div class="m-faq">

	<div class="container">

		<a href="#" class="close">Close</a>

		<h3>
			Frequently Asked Questions
		</h3>

		<p>
			Nam sit amet volutpat sapien. Lorem ipsum dolor sit amet,
			consectetur adipiscing elit. Etiam fringilla consectetur
			orci. Cras enim lectus, mollis ac luctus sit amet,
			dignissim sed ante. Cras non erat massa, sit amet
			elementum sapien. Sed ac sapien sem. Quisque in mauris
			mi. Nulla pharetra accumsan erat.
		</p>

		<p>
			Nam sit amet volutpat sapien. Lorem ipsum dolor sit amet,
			consectetur adipiscing elit. Etiam fringilla consectetur
			orci. Cras enim lectus, mollis ac luctus sit amet,
			dignissim sed ante. Cras non erat massa, sit amet
			elementum sapien. Sed ac sapien sem. Quisque in mauris
			mi. Nulla pharetra accumsan erat.
		</p>

	</div>

</div>
<!-- END: FAQ Module. -->

If anyone has any suggestions, I'm all ears.

Want to use code from this post? Check out the license.

Reader Comments

28 Comments

Nice thinking, as always! A couple of suggestions:

1. Add another variable to keep track of the status of the FAQ module, rather than overloading the faq variable with that responsibility. If you have another variable like faqModuleStatus and values "notloaded", "loading", "loaded" the code should be clearer when you come back to it six months from now.

2. Bear in mind this makes loading a FAQ (potentially) asynchronous. If you want to do anything after loading the FAQ you'll need to add a callback or return a promise. Also, I learned something interesting yesterday: it's better to be always asynchronous than sometimes asynchronous[1]. So add a setTimeout(callback, 0) to make it async in the case where the FAQ module is already loaded.

With these two bases covered, you're much less likely to lose hair trying to figure out why faq doesn't always reference the object you think it does.

[1] https://github.com/joyent/node/blob/a52a44e07299f2b46bd88f73c543828af52e60d0/doc/api/process.markdown#processnexttickcallback

15,848 Comments

@Patrick,

When I first wrote this code, I started to use some sort of separate "State" variable so that I could things like:

if (state === states.LOADING){ ... }

... but I really was just trying to get the code as small as possible. But, definitely, having the "faq" variable bet several different types of value did get under my skin.

Speaking of Promises, however, I'd love to be able to create a more intuitive / seamless approach to the lazy loading. Something where I don't have to do the state checking.

Gotta think about it.

But, I definitely like the idea of "always" being asynchronous rather than "sometimes" asynchronous. I think that probably lends to safer coding.

1 Comments

Cool, this is a good idea for what I'm bulding. I'm defining widgets as an html chunks with id's on the pages and once the main.js gets loaded it looks for those html instances in the dom so it can construct them, so seems like a good idea to lazy load depending if they're in the page or not.

1 Comments

One approach could be to remove the click listener on the first click and add it back when the user closes the overlay. This leads to not needing the intermediate state of loading.

1 Comments

I'm not sure which version of RequireJs you were using, but in my version, calling require(...) twice on the same module will only load the module once, even if subsequent calls occur before the module has loaded.

1 Comments

Hi there, this post is abit older but nevertheless i would like to post a variation based on previous comments (using a Proxy/Singleton factory pattern) for lazy, transparent require

code follows:

<pre>
function Proxy( )
{
this.instances = { };
}
Proxy.prototype = {

constructor: Proxy,

set: function( name, module, initCallback ) {
this.instances[ name ] = [module ? 'loaded' : 'not_loaded', module, initCallback];
return this;
},

get: function( name, action ) {
var reg = this.instances[ name ];
if ( !reg ) return this;
if ( 'loaded' === reg[ 0 ] )
{
setTimeoutaction( function( ){ action( reg[ 1 ] ); }, 0);
}
else if ('not_loaded' === reg[ 0 ] )
{
reg[ 0 ] = 'loading';
require([name], function( module ){
reg[ 0 ] = 'loaded';
reg[ 1 ] = reg[ 2 ] ? reg[ 2 ]( module ) : module;
action( reg[ 1 ] );
});
}
}
};

var proxy = new Proxy( );

proxy.set( "views/faq", null, function( FAQ ){ return new FAQ(); });
// proxy can handle multiple modules, i.e singleton factory pattern
//proxy.set( "views/about", null, function( About ){ return new About(); });
var handleClick = function( event ){
event.preventDefault();
proxy.get("views/faq", function( faq ){ faq.open( body ); } )
};
launchFaq.click( handleClick );
</pre>

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel