James Thomas

Notes on JavaScript

Finding Nano - Getting Dojo Under 4KB

There was a bold claim in the release notes for the 1.7 version of The Dojo Toolkit…

Dojo Nano: Less than 4KB gzipped!

With the move to the AMD module format, the new fully-compliant asynchronous module loader could be reduced to less than four thousands bytes!

Loading unnecessary code was a common complaint against previous versions of The Dojo Toolkit but now we could have complete control over loaded modules using this tiny AMD loader.

Was this true?

Running a standard build to generate a single dojo layer results in a minfied and gzipped file over 45,000 bytes.

How can we generate this nano loader in less than 10% of that size?

Until now, the instructions were spread over mailing list posts, the reference guide and bug tickets, making it possible but not very easy!

There already was an open ticket for the project to ship a complete nano-profile within the sample profiles. Taking up the challenge, I started investigating how to produce a profile that would generate a fully-functional AMD loader in under 4,000 bytes.

Nano-Build Profile

After much experimenting, tweaking and reviewing the toolkit’s source (along with help and advice from other contributors), the smallest usable AMD loader can be produced by running the following build profile.

Once minified and gzipped, the entire loader is only 3652 bytes! Compared to the full loader with base modules, which came in a 45705 bytes, this represents more than a 92% reduction in file size.

So, how does the build profile above squeeze so much space out? Let’s take a closer look at the parameters and explain how they contribute to the reduced size…

Custom Base Layer

Unless specified otherwise, the Dojo build system will always generate a base layer containing the dojo.js source file combined with all the base modules (those defined under the dojo/_base directory).

Generating just the AMD loader, without all those additional modules, needs the profile to contain an explicit definition for the dojo base layer, allowing us to override configuration properties.

Manually defining the base dojo layer is achieved by adding a new configuration object to the layers map, identified with the name dojo/dojo, as shown below.

Base-less loader configuration
1
2
3
4
5
6
layers: {
    "dojo/dojo": {
        include: [],
        customBase: 1
    }
}

Setting the customBase property to true will ensure the build system won’t automatically roll up all the base modules into the nano AMD loader. We’ve left the include property empty as we don’t want to add any extra modules.

This first step in producing a nano loader reduces the minified and gzipped layer by almost 30KB!

Using the Closure Compiler

Dojo’s build system supports the use of different JavaScript minifiers, which perform tricks such as renaming variables and stripping whitespace in order to reduce the size of a JavaScript file.

Shrinksafe is the default minifier, but in our profile we’ve chosen to use Google’s Closure compiler.

Using closure compiler
1
layerOptimize: "closure"

Experimenting with the different minifiers, it was apparent that Closure was more effective at reducing the layer file sizes by the greatest amount.

Closure produces a minified layer file in 35,770 bytes, nearly 10KB less than the original version using Shrinksafe.

More importantly, the Closure compiler supports dead code elimination. Running static analysis over the source files, those code branches which are unreachable will be stripped from the output. This feature is crucial in allowing us to tune the produced loader’s features, squeezing even more space out.

Static Features Configuration

As the Dojo Toolkit moves towards the 2.0 release, one of the major improvements within the code base is the use of dynamic detection for determining which features an environment supports, rather than relying on brittle user-agent sniffing.

Using feature tests, alternative code paths can be executed to provide shim-functionality for missing platform features, using native libraries otherwise. Tests are executed only once, the cached result is returned for each subsequent test.

The build system allows a pre-specified list of feature test results to be provided in the build profile. These parameters will replace the feature test calls within the generated layer files with the static boolean result values.

As this happens before minification, any feature test paths that can’t be executed will be automatically stripped by the Closure compiler. This provides a huge benefit in hand-tuning the loader size to be as compact as possible.

The sample below shows the static feature test results we provide to produce the minimal AMD loader.

Static feature test results
1
2
3
4
5
6
7
8
9
10
11
12
13
staticHasFeatures: {
    'config-dojo-loader-catches': 0,
    'config-tlmSiblingOfDojo': 0,
    'dojo-log-api': 0,
    'dojo-sync-loader': 0,
    'dojo-timeout-api': 0,
    'dojo-sniff': 0,
    'dojo-cdn': 0,
    'dojo-loader-eval-hint-url': 1,
    'config-stripStrict': 0,
    'ie-event-behavior': 0,
    'dojo-config-api': 0
}

Using static features configuration allows us to remove all non-essential code needing for loading AMD modules. This includes the synchronous module loader code used to load non-AMD modules (dojo-sync-loader), the debugging methods for module loading (dojo-timeout-api and dojo-log-api), backwards compatibility for non-standard DOM event behaviours (ie-event-behaviour) and others.

Full details on each of the feature tests defined in the toolkit will be available in the 1.8 reference guide, see here for a sneak preview.

Hand tuning the static feature test results allowed the build to remove an extra 2,000 bytes from the nano loader.

Baking in Default Configuration

Making the smallest AMD loader possible relies on a series of assumptions about the environment we’ll be running in and supported features. Rather than have the user set these values manually, we can hard code this configuration into the loader, allowing us to remove the code for parsing configuration values from the environment.

The following configuration is provided within the nano profile.

Default loader configuration
1
2
3
4
5
6
7
8
9
10
defaultConfig:{
    hasCache:{
        'dojo-built': 1,
        'dojo-loader': 1,
        'dom': 1,
        'host-browser': 1,
        'config-selectorEngine': 'lite'
    },
    async:1
}

Along with configuration for the environment (modern-ish browser engine), we’ve set the async property to true, ensuring the loader is running in AMD-mode as we’ve removed all code for handling the legacy Dojo module format.

Squeezing Out Those Final Bytes

So, what’s left?

How can we squeeze a few more bytes out?

Reviewing the source code for the build system, when the dojo layer is generated, the following boot sequence is appended to the source.

Dojo boot text
1
2
3
4
5
6
// must use this.require to make this work in node.js
var require = this.require;
// consume the cached dojo layer
require({cache:{}});
!require.async && require(["dojo"]);
require.boot && require.apply(null, require.boot);

This code ensures the loader will work on the NodeJS platform and ensures that all base modules are always requested when running in legacy mode.

Our minimal loader doesn’t need to run outside the browser and we definitely won’t be running in legacy mode! Therefore, we can overwrite the layer boot text with custom code to trim the last few bytes from the nano loader, shown below.

Custom boot text
1
dojoBootText:"require.boot && require.apply(null, require.boot);",

…and that’s it! Combining all of the options above results in a fully-functioning AMD loader in less than 4 kilobytes.

For further details on the exact size reductions achieved by each of the profile parameters, see this link for the data.

Differences between nano-profile and profile included with toolkit

The profile defined above will produce the smallest functional AMD loader possible, sacrificing support for certain common features to reduce the file size even further. When producing the nano profile that will be shipped with the toolkit, there’s a slightly less aggressive approach when balancing feature completeness against file size.

Reviewing the feature tests, we decided that two optional features should be included, backwards compatibility for older Internet Explorer browsers (ie-event-behaviour) and the ability for manual loader configuration (dojo-config-api). These changes only produce an additional 900 bytes and will make the minimal loader much more consumable.

The nano build profile shipped with the toolkit also contains all configurable feature values, rather than just the minimal set needed to produce the smallest build, to demonstrate the full set of parameters that can be modified.

More information about the investigations into producing this profile can be found in the contributors mailing list thread here.

Finally…

This investigation was founded upon previous work by other dojo contributors. Thanks to Ben Lowery, Kitson Kelly and Rawld Gill for their initial efforts and helping me out with questions.

Comments