Lazy Loading RequireJS Modules When They Are First Requested

Posted July 24, 2012 at 10:48 AM by Ben Nadel

Tags: Javascript / DHTML

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.




Reader Comments

Jul 24, 2012 at 1:37 PM // reply »
26 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


Jul 24, 2012 at 2:09 PM // reply »
11,238 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.


Jul 25, 2012 at 1:38 PM // reply »
11,238 Comments

@All,

I've tried to factor out the "noise" in this demo into its own module:

http://www.bennadel.com/blog/2403-Creating-A-Lazy-Loading-Utility-Module-For-RequireJS.htm

I created a utility module just for handling the state management / transitions for the asynchronous loading of the RequireJS modules.


Nov 27, 2012 at 11:51 PM // reply »
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.


Nov 29, 2012 at 1:27 AM // reply »
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.


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 19, 2013 at 2:31 PM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
It's funny really just how well that image describes the way I would imagine most people that go with angular for some project is. I have had a similar roller-coaster ride with it as well, but not qu ... read »
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools