Minimal AMD loader
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:
- works in any modern browser without using any ES6 features like Promises,
- exposes basic AMD functions:
define
andrequire
, - is designed to be as small as possible after compression with Uglify.js while still being readable,
- can be extended by defining new loader plugins,
- 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 };
});
function assert(condition) {
if (!condition) {
throw new Error('Assertion failed');
}
assert.called = true;
}
if (editor) {
var code = editor.get(), uglified = uglify.compress(code);
minimized.textContent = uglified;
minimizecount.textContent = uglified.length;
if (typeof tests !== 'undefined') {
tests.textContent = 'failed';
tests.className = 'tests failure icon-cancel';
try {
var require, define, window = { };
eval(code);
require = require || window.require;
define = define || window.define;
eval(unitTests.get());
if (assert.called) {
tests.textContent = 'passed';
tests.className = 'tests success icon-ok';
} else {
tests.textContent = 'failed: No assertions';
}
} catch (e) {
tests.textContent = 'failed: ' + e;
}
}
countTotal();
}
function countTotal() {
// count total
var totalCount = Array.prototype.map.call(document.querySelectorAll('.minimize-final-count'), function(el) {
return el.textContent;
}).reduce(function(a, b) { return Number(a) + Number(b); }, 0);
document.querySelector('.total-count').textContent = totalCount || "...";
}
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
):
define
— defines a module when all dependencies are available,require
— executes a callback when dependencies are loaded.
(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:
- 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, - replacing
null
inapply
andbind
calls with 0 — asthis
argument is not used in these functions this replacement will save 3 characters for each parameter, - changing
delete listeners[name]
tolisteners[name] = 0
— that will save 3 characters while still allowing listener functions to be garbage collected, - naming the
require
functionreq
and using this name indefine
— 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:
js
— used to load external JavaScript modules by adding<script>
tags. The modules will contain appropriatedefine
calls so this loader is very simple.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.
…
function assert(condition) {
if (!condition) {
throw new Error('Assertion failed');
}
assert.called = true;
}
var minimized = uglify.compress(code);
minimized.textContent = minimized;
minimizecount.textContent = minimized.length;
countTotal();
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
Revisions
- Initial version.