An Introduction to Javascript Modules and Bundling

Modules

A module is a way of dividing or grouping your code into files or sections. Modules are ideally self contained and decoupled, so that changes can be made in a single module without having to rewrite other modules.

Why Use Modules?

Modules have a number of benefits including namespacing, better maintainability, and reusability.

Without modules, it’s very easy to get namespace pollution because all variables outside of top level functions are global and accessible everywhere in the code, even if it’s unrelated. Modules allow namespacing, which creates private variables to reduce or eliminate this pollution.

Modules also allow for decoupling of unrelated code. This means that changes in one file should not require changes to be made in other files, resulting in more maintainable code.

Modules allow you to import and export a single module, so that it can be reused as many times as you would like without duplicating the code. This is great because it means that any required changes will only be needed in one spot, instead of all the potential duplicated places.

Image courtesy of Librato
Image courtesy of Librato

Using Modules

In Javascript, the only way to get a new scope is through functions. Functions can provide namespacing by returning methods or variables that are to be exposed and not returning anything that is needed to be private.

Please see Javascript Modules: A Beginners Guide for examples.

However, these methods are still not perfect, as they only reduce the problem instead of solving it. You will have less globals, but all of the top level functions can still be accessed throughout the entire codebase. Namespace pollution and collisions (two modules with the same name, or a single module with two versions still being used) can still occur. With large code bases, dependency management also becomes difficult as dependencies have to be loaded in the browser before the code requiring it.

External Tools for Modules

Until ES6, there was no native way in Javascript to import and export modules (similar to Ruby’s “require” and Python’s “import” statements). However, there are external tools such as CommonJS and AMD that allow for importing and exporting modules without using the global scope.

CommonJs

With CommonJs, you can export only those objects that you want to expose, which can then be required or imported into other modules where they are needed. This makes the code reusable, because you can require the exported modules as many times as needed. With CommonJs you avoid namespace pollution and make dependencies explicit. When loading files into the browser, CommonJs uses a server side synchronous approach. This means that each required module is loaded one at a time, and the browser needs to wait for the entire module to load when needed before moving on. This can be a source of slow loading on websites, but there are some workarounds.

AMD (Asynchronous Module Definition)

To import modules using AMD, you use a “define” statement which takes as parameters a list of modules that are being imported, and a callback function that is executed once the modules have loaded. Using the callback approach allows AMD to take a browser-first asynchronous approach to loading modules, in contrast to CommonJs’ server-first synchronous approach. With AMD you can import objects, functions, constructors, strings and other types whereas CommonJs only supports objects as modules. However, AMD isn’t compatible with io or filesystem, and is missing other server side features that CommonJs has.

UMD (Universal Module Definition)

In cases where you need both synchronous and asynchronous module loading you can use UMD. With UMD you can use either CommonJS or AMD.

ES6

ES6 supports modules natively for javascript. It uses asynchronous loading (like AMD), and handles cyclic dependencies well. In ES6, imported modules are live-read-only views of the exported module, in contrast to CommonJs which has a disconnected copy for both export and import. Read more about this at Javascript Modules: A Beginners Guide and Javascript Modules: the ES6 Way.

Bundling

Bundling is concatenating all your modules into a single file in the right order for dependencies.

Why Bundle?

Modules and libraries are all in separate files, which means that each file requires a <script> tag in the HTML. The HTML file is loaded on page load, and separate tags for each file means the module and library files would also have to be loaded one by one. This can be a time consuming process as projects get large. In order to reduce the number of requests for loading all the modules, you can bundle them into one or a few (if there are a lot) files. This is known as the build step. As part of bundling you can also minify the code, which is removing any unnecessary characters such as new lines, white spaces and comments. This also helps speed up bundling and the build process.

Bundling is straightforward when you’re using vanilla javascript, but becomes complex when using non-native modules such as CommonJs, AMD and ES6. Browsers cannot interpret these types of modules, so tools such as Webpack and Browserify can be used to convert the code into browser readable code.

Bundling CommonJs

As previously mentioned, CommonJs uses synchronous loading which can lead to a time consuming page load. One of the common workarounds for this issue in Browserify. Browserify compiles CommonJs for browsers. First Browserify parses the abstract source tree for each dependency starting at the root file, once it has figured out the dependency order it bundles the modules into a single file. Outputting a file ready to be minified. Now you only need to add one <script> tag into the HTML, dramatically reducing load time for large projects.

Bundling AMD

AMD requires a module loader such as Curl or RequireJS instead of a bundler. Module loaders dynamically load modules as they are needed. Bundling is less important with AMD since it’s asynchronous and progressively downloads required files instead of loading all the files on page load. In reality overtime, it becomes costly for each user action to have to wait for necessary files to be loaded thus bundling and minifying can also be used for AMD.

Webpack

Webpack is an agnostic module bundler that works with CommonJs, AMD and ES6. One major advantage it provides is code-splitting, this is when code is split into chunks and the chunks are loaded on demand. For example it may not be efficient to load and bundle code that is only executed under a certain condition, so webpack can chunk and bundle this code separately and only load it when needed.

Bundling ES6

ES6 works in a different way than the methods described so far. In this process during compilation, exports not being used by other modules can be removed via a method called tree shaking. With the use of tree shaking only code that is imported and can be executed is included in the bundles.

Since there is still no native implementation of how browsers use ES6, making ES6 browser friendly requires a bit of extra work. There are a few different ways to do this, one of which is using transpiler. A transpiler such as Babel or Traceur compiles ES6 code into ES5 (can use CommonJS, AMD or UMD) which can then be put through bundler such as Webpack.

Conclusion

Modules help organize code, making it easier to maintain and reuse. Prior to ES6 there were no native javascript modules, so tools such as CommonJs and AMD were used to create import and export modules. Modules can then be bundled with use of Webpack, Browserify, or something similiar to reduce load times of pages. In the case of ES6 an extra step is required to make the code browser friendly, the most common way of doing this is transpiling.

Further Reading

About the Author
Amy-NewAmy is a Co-op developer on Hootsuite’s Dashboard team. She attends UBC. When not coding, she enjoys petting dogs and not writing bios for her blog post, leaving her editor to do it. Connect with her on LinkedIn.