Minimal AMD loader

comment

AMD simplifies modularizing JavaScript applications but as it is not a browser built-in mechanism a loader library is still needed to bootstrap the process. Fortunately it is possible to write a loader that supports plugins in less than 850 characters of optimized code.

The loader implemented here:

  1. works in any modern browser without using any ES6 features like Promises,
  2. exposes basic AMD functions: define and require,
  3. is designed to be as small as possible after compression with Uglify.js while still being readable,
  4. can be extended by defining new loader plugins,
  5. does not preserve compatibility with require.js.

The following unit test will check whether the loader works correctly.

define('framework', ['component', 'library'], function(cmp, lib) {
    return { init: 'initialized:\ncomponent: ' + cmp.description +
        '\nand library: ' + lib.version};
});

require(['framework'], function(framework) {
    assert(framework.init === 'initialized:\ncomponent: uses library version: 0.0.1\nand library: 0.0.1');
});

define('library', [], function() {
    return { version: '0.0.1' };
});

define('component', ['library'], function(lib) {
    return { description: 'uses library version: ' + lib.version };
});

Implementation

The implementation consists of two parts — internal functions and external functions.

Internal functions take care of the low-level dependency definitions and listeners:

  • addLoadListener — registers a callback to be executed when a dependency has been defined. If it is already defined the callback is executed immediately,
  • resolve — sets a dependency value and notifies all listeners.

External functions are implemented in terms of the internal ones and are exported to the global object (window):

(function() {

    var registry = {
        listeners: { },
        resolves: { }
    };

    function addLoadListener(name, listener) {
        if (name in registry.resolves) {
            // value is already loaded, call listener immediately
            listener(name, registry.resolves[name]);
        } else if (registry.listeners[name]) {
            registry.listeners[name].push(listener);
        } else {
            registry.listeners[name] = [ listener ];
        }
    }

    function resolve(name, value) {
        registry.resolves[name] = value;
        var libListeners = registry.listeners[name];
        if (libListeners) {
            libListeners.forEach(function(listener) {
                listener(name, value);
            });
            delete registry.listeners[name];
        }
    }

    window.require = function(deps, definition) {
        if (deps.length === 0) {
            // no dependencies, run definition now
            definition();
        } else {
            // we need to wait for all dependencies to load
            var values = [], loaded = 0;
            function dependencyLoaded(name, value) {
                values[deps.indexOf(name)] = value;
                if (++loaded >= deps.length) {
                    definition.apply(null, values);
                }
            }
            deps.forEach(function(dep) {
                addLoadListener(dep, dependencyLoaded);
            });
        }
    }

    window.define = function(name, deps, definition) {
        if (!definition) {
            // just two arguments - bind name to value (deps) now
            resolve(name, deps);
        } else {
            // asynchronous define with dependencies
            require(deps, function() {
                resolve(name, definition.apply(null, arguments));
            });
        }
    }

}());

Unit tests: .

Minimized using Uglify.js to characters.

Optimization

Although the original implementation works and is already small it can be restructured so that Uglify.js produces even smaller code.

Flattened variables

The minimized output contains several names that are not shortened — listeners and resolves. Uglify does not modify them because they are parts of the variable registry. If this variable was passed to external code then the mangled names could cause unexpected behavior. Uglify does not know that this object is internal and never minifies its attributes. One solution to this problem is manually minifying attribute names for internal objects.

Another solution is flattening the object structure. Instead of having listeners and resolves nested in registry they will be converted to variables.

Implementation with flattened registry object (only addLoadListener and resolve are changed).

(function() {

    var listeners = { }, resolves = { };

    function addLoadListener(name, listener) {
        if (name in resolves) {
            // value is already loaded, call listener immediately
            listener(name, resolves[name]);
        } else if (listeners[name]) {
            listeners[name].push(listener);
        } else {
            listeners[name] = [ listener ];
        }
    }

    function resolve(name, value) {
        resolves[name] = value;
        var libListeners = listeners[name];
        if (libListeners) {
            libListeners.forEach(function(listener) {
                listener(name, value);
            });
            delete listeners[name];
        }
    }

    window.require = function(deps, definition) {
        if (deps.length === 0) {
            // no dependencies, run definition now
            definition();
        } else {
            // we need to wait for all dependencies to load
            var values = [], loaded = 0;
            function dependencyLoaded(name, value) {
                values[deps.indexOf(name)] = value;
                if (++loaded >= deps.length) {
                    definition.apply(null, values);
                }
            }
            deps.forEach(function(dep) {
                addLoadListener(dep, dependencyLoaded);
            });
        }
    }

    window.define = function(name, deps, definition) {
        if (!definition) {
            // just two arguments - bind name to value (deps) now
            resolve(name, deps);
        } else {
            // asynchronous define with dependencies
            require(deps, function() {
                resolve(name, definition.apply(null, arguments));
            });
        }
    }

}());

Unit tests: .

Minimized using Uglify.js to characters.

Partial functions

Several instances of anonymous functions can be shortened by using a partially applied function created by bind. One example is the anonymous function used in window.require above.

Unfortunately as bind fixes left-most arguments the order of parameters in the inner call to addLoadListener needs to be changed — first the listener then the name.

From this code:

deps.forEach(function(dep) {
    addLoadListener(dep, dependencyLoaded);
});

The order of parameters change:

deps.forEach(function(dep) {
    addLoadListener(dependencyLoaded, dep);
});

And the anonymous function can be removed by creating a partially applied function from addLoadListener:

deps.forEach(addLoadListener.bind(null, dependencyLoaded));

bind returns a function that when called with one argument value will call addLoadListener with two arguments: dependencyLoaded and value.

Finally the dependencyLoaded function can be inlined as it is not used anywhere else.

deps.forEach(addLoadListener.bind(null, function(name, value) {
    values[deps.indexOf(name)] = value;
    if (++loaded >= length) {
        definition.apply(null, values);
    }
}));

Introduce variables

Careful examination of the optimized output shows that the length of the dependencies array is retrieved twice inside the require function. The name of the length attribute cannot be optimized but using a variable to store and read it twice will save several bytes.

The comparison of length with 0 can be minimized to !length as 0 is falsy leading to this code:

window.require = function(deps, definition) {
    var length = deps.length;
    if (!length) {
        // no dependencies, run definition now
        definition();
    } else {
        // we need to wait for all dependencies to load
        var values = [], loaded = 0;
        deps.forEach(addLoadListener.bind(null, function(name, value) {
            values[deps.indexOf(name)] = value;
            if (++loaded >= length) {
                definition.apply(null, values);
            }
        }));
    }
}

Other small changes

These will decrease the output size slightly but may reduce code readability:

  1. removing assignments to window — using assignments to undeclared variables will result in assignments made to the global object if not running under the strict mode,
  2. replacing null in apply and bind calls with 0 — as this argument is not used in these functions this replacement will save 3 characters for each parameter,
  3. changing delete listeners[name] to listeners[name] = 0 — that will save 3 characters while still allowing listener functions to be garbage collected,
  4. naming the require function req and using this name in define — the inner name can be optimized and will save further 3 characters.

Final optimized code

(function() {

    var listeners = { }, resolves = { };

    function addLoadListener(listener, name) {
        if (name in resolves) {
            // value is already loaded, call listener immediately
            listener(name, resolves[name]);
        } else if (listeners[name]) {
            listeners[name].push(listener);
        } else {
            listeners[name] = [ listener ];
        }
    }

    function resolve(name, value) {
        resolves[name] = value;
        var libListeners = listeners[name];
        if (libListeners) {
            libListeners.forEach(function(listener) {
                listener(name, value);
            });
            // remove listeners (delete listeners[name] is longer)
            listeners[name] = 0;
        }
    }

    function req(deps, definition) {
        var length = deps.length;
        if (!length) {
            // no dependencies, run definition now
            definition();
        } else {
            // we need to wait for all dependencies to load
            var values = [], loaded = 0;
            deps.forEach(addLoadListener.bind(0, function(name, value){
                values[deps.indexOf(name)] = value;
                if (++loaded >= length) {
                    definition.apply(0, values);
                }
            }));
        }
    }

    /** @export */
    require = req;

    /** @export */
    define = function(name, deps, definition) {
        if (!definition) {
            // just two arguments - bind name to value (deps) now
            resolve(name, deps);
        } else {
            // asynchronous define with dependencies
            req(deps, function() {
                resolve(name, definition.apply(0, arguments));
            });
        }
    }

}());

Unit tests: .

Minimized using Uglify.js to characters.

Plugins

The loader plugins are used to retrieve dependencies that are not scripts or have specific loading needs. For example CodeMirror requires styles to be present before embedding the editor.

Dependency name can contain an exclamation mark (!) that indicates which loader plugin to use — css!codemirror/styles.css means that the css plugin will load the codemirror/styles.css file.

For simplicity all external dependencies — even scripts — will need plugin prefix (require.js doesn't have this limitation). Loader plugins themselves will be initialized using js plugin because all plugins are scripts.

The only modification in the minimal loader implemented above is loading the plugin when a dependency is requested for the first time and then delegating the loading process to it.

function addLoadListener(listener, name) {
    if (name in resolves) {
        // value is already loaded, call listener immediately
        listener(name, resolves[name]);
    } else if (listeners[name]) {
        listeners[name].push(listener);
    } else {
        listeners[name] = [ listener ];

        // first time this dependency is requested
        // get the loader name from string before ! character
        req([ 'js!' + name.split('!')[0] ], function (loader) {
            loader(name);
        });
    }
}

The following code defines two plugins:

  1. js — used to load external JavaScript modules by adding <script> tags. The modules will contain appropriate define calls so this loader is very simple.
  2. css — used for style sheets. Before returning this plugin will check if styles are being applied on the page and waits until they do.
(function(document, define, setTimeout) {

function addElement(name, properties) {
    var element = document.createElement(name);
    for (var item in properties) {
        element[item] = properties[item];
    }
    document.head.appendChild(element);
}

define('js!js', function(name) {
    var fileName = name.split('!')[1];
    addElement('SCRIPT', {
        src: fileName
    });
});

define('js!css', function(name) {
    var fileName = name.split('!')[1];
    addElement('LINK', {
        href: fileName,
        rel: 'stylesheet',
        onload: function check() {
            for (var i = 0, sheet; sheet = document.styleSheets[i]; i++) {
                if (sheet.href && (sheet.href.indexOf(fileName) > -1)) {
                    return define(name);
                }
            }
            // style is loaded but not being applied yet
            setTimeout(check, 50);
        }
    });
});

// require dependencies specified in <body data-load attribute
setTimeout(require.bind(0,
    document.body.getAttribute('data-load').split(' '), Date), 0);

}(document, define, setTimeout));

Minimized using Uglify.js to characters.

This fragment of code uses several new techniques to minimize the output.

Copy globals

Global symbols that are used more than once are copied into function’s arguments and thus they can be minimized saving 30 characters in this case.

(function(document, define, setTimeout) {

// document, define and setTimeout are used more than once here
// because they are arguments Uglify.js will minimize each usage

}(document, define, setTimeout));

Use Date as an empty function

The require function expects two arguments — array of dependencies and a function to call when these dependencies are loaded. As dependencies are not used here instead of using empty function (function(){}) a side-effect free Date function is used saving 8 characters.

require(document.body.getAttribute('data-load').split(' '), Date);

Partial application

The main require call is delayed by setTimeout. That allows the browser to paint the initial page layout using inline styles and load the rest of them in background. Interactive scripts and additional styles are therefore removed from the critical rendering path.

To avoid having an anonymous function for setTimeout a new partially applied function is created from require. This new function is parameterless and when invoked will call require with an array of dependencies and the callback function Date.

setTimeout(require.bind(0,
    document.body.getAttribute('data-load').split(' '), Date), 0);

Loader

Minimized loader has only characters but if you see a way to improve it further don't hesitate to send your suggestions.

Currently this minimized loader is used on this blog (see end of this page’s HTML).

Comments

Todd Motto (@toddmotto)
Minimal AMD loader in 850 characters, awesome read.
@luke_redroot
Fascinating article on writing a AMD Loader in a minimal way.
cancel

Revisions

  1. Initial version.