Over the last few years, developers have been relentlessly moving their server-side sites to the client-side on the premise that the page performance would be improved.

However, this may not be enough. Have you ever considered that your site may be downloading more stuff than is being actually used? Meet Lazy-loading, a design pattern about deferring the initialization(loading/fetching/allocation) of a resource (code/data/asset) until the point at which it is needed.

At the same time, ES2015 is already production-ready through transpilers such as Babel. Now you no longer need to fight the AMD vs CommonJS war - described on my article The mind-boggling universe of JavaScript Module strategies - since you can simply write ES2015 modules and have them transpiled and delivered to the browser, while supporting your existing CommonJS or AMD modules.

This article demonstrates how to load ES2015 modules synchronously (during the page load) and asynchronously (performing lazy-loading) using System.js.

Page Load vs Lazy-Loading

When developing JavaScript code to be executed on the browser, you always have to decide WHEN you want it to be executed.

There is always some chunk of code that must run during the page load, as for instance the structural setup of an SPA using frameworks such as Angular, Ember, Backbone or React. Such code must be referenced on the main HTML document returned to the browser after a page request, most likely through one or more <script> tags.

On the other hand, you might have some more chunks of code from features that should only be executed if certain triggering conditions happen. Classical examples are:

  • Content below the fold, such as a Reviews panel, which shows up after the user scrolls down the page.
  • Content displayed as a consequence of triggering an event, such as a Zoomer overlay, which shows up after the user clicks on the image.
  • Unusual/infrequent content, such as a Free Shipping widget, which only applies to a fraction of the pages
  • Content that shows up after some time, such as a customer service chat box.

This way, for a given feature like those ones above, if its triggering condition never happens, its chunk of code won't ever be executed. Hence, that chunk of code is definitely not needed during the page load and it can be deferred.

In order to defer it, you simply need to leave that code out of the chunk of code which gets downloaded and executed during the page load - so that it would only be downloaded and executed on demand, when its triggering condition happens for the first time.

This approach of asynchronously loading deferred code, or lazy-loading, plays an important role in improving the page performance, in terms of reducing the page load time and the Speed Index.

In order to learn more about the Speed Index and the performance impacts on Page load vs Lazy-loading, check out my article: Leveling up: Simple Steps to Optimize the Critical Rendering Path.

The Pitfalls of AMD

The AMD standard was created for asynchronous module loading on the browser, being one of the first successful alternatives to the spaghetti mess known as global JavaScript files scattered around your page. According to the Require.js documentation:

The AMD format comes from wanting a module format that was better than today’s “write a bunch of script tags with implicit dependencies that you have to manually order” and something that was easy to use directly in the browser.

It is based on empowering the Module Design Pattern with a module loader, dependency injection, alias resolution and asynchronous capabilities. One of it main usages is to perform lazy-loading of modules.

Despite being a formidable idea, it brings some inherent complexity: namely, the need to understand runtime module timelines, which was previously unnecessary. This means that developers need to know when each asynchronous module is expected to do its work.

By failing to understand this, developers got into situations that may work sometimes and may not work some other times, due to race conditions, which can be quite difficult to debug. Because of such things, AMD lost quite a bit of its momentum and traction, unfortunately.

In order to learn more about AMD pitfalls, check out Moving Past RequireJS.

ES2015 modules 101

Before we go any further, let's go over ES2015 modules. If you are already familiar with them, here's a quick refresher.

Modules have been finally adopted as an official part of the JavaScript language in ES2015. They are powerful yet simple to grasp, standing on the shoulders of the CommonJS modules giants.

Scope

Basically, a ES2015 module will live in its own file. All its "globals" variables will be scoped to just this file. Modules can export data and also import other modules.

Exporting and Importing

Export a ES2015 module's interface through the keyword export before each item you want to export (a variable, function or class). In the following example, we are exporting Dog and Wolf:

// zoo.js
var getBarkStyle = function(isHowler) {  
  return isHowler? 'woooooow!': 'woof, woof!';
};

export class Dog {  
  constructor(name, breed) {
    this.name = name;
    this.breed = breed;
  }

  bark() {
    return `${this.name}: ${getBarkStyle(this.breed === 'husky')}`;
  };
}

export class Wolf {  
  constructor(name) {
    this.name = name;
  }

  bark() {
    return `${this.name}: ${getBarkStyle(true)}`;
  };
}

Let's see how to import this module in a Mocha/Chai unit test, using the syntax import <object> from <path>. As for <object> we can pick which elements we want to import - something called named imports. We can then decide to just import expect from chai as well as Dog and Wolf from Zoo. By the way, this syntax of named imports resembles another handy ES2015 feature - destructuring objects.

// zoo_spec.js
import { expect } from 'chai';  
import { Dog, Wolf } from '../src/zoo';

describe('the zoo module', () => {  
  it('should instantiate a regular dog', () => {
    var dog = new Dog('Sherlock', 'beagle');
    expect(dog.bark()).to.equal('Sherlock: woof, woof!');
  });

  it('should instantiate a husky dog', () => {
    var dog = new Dog('Whisky', 'husky');
    expect(dog.bark()).to.equal('Whisky: woooooow!');
  });

  it('should instantiate a wolf', () => {
    var wolf = new Wolf('Direwolf');
    expect(wolf.bark()).to.equal('Direwolf: woooooow!');
  });
});

Default

If you only have one item to export, you can use export default to export your item as an object instead of exporting a container object with your item inside:

// cat.js
export default class Cat {  
  constructor(name) {
    this.name = name;
  }

  meow() {
    return `${this.name}: You gotta be kidding that I'll obey you, right?`;
  }
}

Importing default modules is simpler, as object destructuring is no longer needed. You can simply directly import the item from the module.

// cat_spec.js
import { expect } from 'chai';  
import Cat from '../src/cat';

describe('the cat module', () => {  
  it('should instantiate a cat', () => {
    var cat = new Cat('Bugsy');
    expect(cat.meow()).to.equal('Bugsy: You gotta be kidding that I\'ll obey you, right?');
  });
});

In order to learn more about ES2015 modules, check out Exploring ES6 book — Modules.

ES2015 Module Loader and System.js

As surprising as it may be, ES2015 doesn't actually have a module loader specification. There was a popular proposal for a dynamic module loader - es6-module-loader - which inspired System.js. This proposal has been retreated, but there is both a new Loader spec in the works by WhatWG, and the Dynamic Import spec by Domenic Denicola.

Nevertheless, System.js is today one of the most frequently used module loader implementations which support ES2015. It supports ES2015, AMD, CommonJS and global scripts in the browser and NodeJS. It provides an asynchronous module loader (to pair with Require.js) and ES2015 transpiling through Babel, Traceur or Typescript.

System.js implements asynchronous module loading using a Promises-based API. This is a very powerful and flexible approach, since promises can be chained and combined: so for instance, if you want to load multiple modules in parallel, you can use Promises.all and just fire your listener when all the promises have been resolved.

Lastly, the Dynamic Import spec is getting a lot of traction and has been incorporated on Webpack 2. You can check out how it's going to work on Webpack 2's guide for Code splitting with ES2015. It's also inspired in System.js so the transition would be quite simple.

Importing Modules Synchronously and Asynchronously

In order to illustrate the loading of modules in both synchronous and asynchronous fashion I've created a sample project, which will synchronously load our Cat module during the page load, and lazy-load the Zoo module once the user clicks on a button. The code is available on my Github project lazy-load-es2015-systemjs.

Let's have a look at the main chunk of code which is loaded during the page load, our main.js.

First, notice how it performs synchronous loading of Cat through import. After that, it creates an instance of Cat, invokes its method meow() and append the result to the DOM:

// main.js
// Importing Cat module synchronously
import Cat from 'cat';

// DOM content node
let contentNode = document.getElementById('content');

// Rendering cat
let myCat = new Cat('Bugsy');  
contentNode.innerHTML += myCat.meow();  

Lastly, notice the asynchronous loading of Zoo through System.import('zoo'), and finally, the instances of Dog and Wolf invoking their method bark() and again appending the results to the DOM:

// Button to lazy load Zoo
contentNode.innerHTML += `<p><button id='loadZoo'>Lazy load <b>Zoo</b></button></p>`;

// Listener to lazy load Zoo
document.getElementById('loadZoo').addEventListener('click', e => {

  // Importing Zoo module asynchronously
  System.import('zoo').then(Zoo => {

    // Rendering dog
    let myDog = new Zoo.Dog('Sherlock', 'beagle');
    contentNode.innerHTML += `${myDog.bark()}`;

    // Rendering wolf
    let myWolf = new Zoo.Wolf('Direwolf');
    contentNode.innerHTML += `<br/>${myWolf.bark()}`;
  });
});

Voilà

When the page is first loaded, the only modules which are loaded are Cat and Main:

Cat and Main modules.png

 Once the user clicks on the button, the Zoo module is then loaded:

Zoo module loaded.png

Conclusion

Mastering the art of keeping the page load close to the minimal necessary and lazy-loading deferrable modules can definitely improve your page performance. AMD and CommonJS paved the way for ES2015 modules, which are available to you right now via transpilers. You can start loading your ES2015 modules with System.js, or with the Dynamic Imports spec over Webpack 2, while the official solution is not yet released.

For more information on this, check out the presentation Lazy Loading ES2015 modules in the browser given by the author at conferences such as: Front End Design Conference (St. Petersburg, FL), DevCon5 (New York, NY) and Abstractions (Pittsburgh, PA).

 

Lazy load meme .jpg

 


Author

Tiago Garcia

Tiago Garcia is a Technology Manager at Avenue Code. He's a JavaScript P.I., dog daddy, vegan rollerblader, and not your typical Brazilian, though he was the first employee in Brazil. He's also a conference speaker and article writer.


What are CSS Custom Properties

READ MORE

How to Build Your Own Facial Recognition Web App

READ MORE

4 Ways to Make Your Angular App Sustainable and Scalable

READ MORE

How to Build a Custom Component in VueJS

READ MORE