Tutorial on the basics of NextJS for building a website.
ECMAScript Asynchronicity - dynamic import
Imagine you are developing a large scale web application, with several thousands of lines of code, and dozens of dependencies. And now you are happy that you're finally building your application to be ready for production. Once you create your bundle file and load it in the page, your application might work just fine. However, because life is full of unpleasant surprises, your app might be just another disappointment and you will end up feeling annoyingly uncomfortable.
Why is that? Your bundle, my friend, is nothing less than a massive file which requires too much time in order to be loaded in your page. Given some, not so glorifying, browsers performance, you're gonna need to address the situation.
Fortunately for you, there are some good folks out there working on stuff that can help you, stuff like code splitting. They make sure your app is loaded in several chunks, as small as possible, in order to accelerate the loading. The tools that provide this kind of features are: RequireJS, SystemJS, Webpack, Rollup and curl. They are capable of bundling your app and generating your bundle chunks, and especially lazy loading them, so you can load only the one that you need at a given time.
Therefore, the use of dynamic import is necessary. Its main purpose is to optimize the amount of loaded code by lazy loading modules.
Since we're talking about modules, let's take a look at them.
ECMAScript provides a module system that is similar to that of Node. Its modules are represented by simple files, and each module has its own context. This means that whatever variables, functions, etc, you declare inside of these files, they won’t pollute the global context.
The code above declares 2 local functions and exports an anonymous one. We can't use the local functions outside of this module. In the module below, we only have access to what
add.js exports, namely the anonymous function, which we are renaming to
The ascension of ES6 made it possible to put an end to the choice between the two protagonist systems of ES5:
AMD. ES6 system has a declarative syntax, which makes it clear and simple. It combines their benefits, and provides an intuitive syntax that makes it easy for engineers to handle.
It even goes beyond the capabilities of ES5 system by using both synchronous and asynchronous loading, along with a static module structure. That is, you need to explicitly specify what you are importing, by using module names instead of dynamic variables. So, the following is not recommended:
The static aspect of ES6 modules comes up with some great benefits:
- It makes it easy for bundlers to eliminate unused modules and de-duplicate redundant ones when bundling. This is called Tree Shaking —which was made popular by the module bundler Rollup.
- Allows cyclic dependencies between modules.
- Provides variable checking that we can think of as a "shallow type checking", which will give us the opportunity to early catch common errors.
- Gives the possibility to add static type checking in future versions of ECMAScript.
For further reading on modules, check Dr. Axel Rauschmayer's online book on modules.
Code splitting with Webpack
Webpack offers several features to optimize your application's bundle. Code splitting is among these features. It can be done in 2 different ways: declarative and imperative. The declarative way generates several bundles based on the entries you specify in Webpack's config, while the imperative way generates bundles based on dynamic imports in your code. Let's see how the declarative one is done:
Here is a classical Webpack config file:
After building our app, Webpack generates only one bundle,
main.js, with its source map
main.js.map. And, thanks to the
compression-webpack-plugin, we have also those files "gzip"ed.
One of the ways you can split your bundle is by defining entry points in Webpack config. These entry points represent the chunks that will be generated. Another way is by using CommonsChunkPlugin. In the following example, we’re going to use both ways.
How to choose your entry points is totally up to you. In our case, we will adopt a strategy that will help us isolate vendor libraries in a single chunk. Then, we create another chunk only for our app’s code.
In our entry property, we're specifying 2 entry points
styles, and we're using the
CommonsChunkPlugin to intercept vendor modules, so that we can isolate them in a single chunk
vendor.js. This is done by the
minChunks function of the plugin.
Until now we've only seen how to split our code at compile time, how about runtime?
Lazy loading is a much cooler feature than simple code splitting; not only it splits your code, but loads only the chunks you need. It allows you to incrementally load your app. This is a piece of cake for ECMAScript's
import(), but before getting there, let's see how the legacy way was:
In the following example we will see how to asynchronously load the
StoryEditor component from
Here is the Editor component that loads the
StoryEditor component asynchronously, using the
componentDidMount lifecycle method we use
require.ensure to load and make available the
StoryEditor component. Then, we use the static
require to extract and display it.
So, when we execute our code, the file that is loaded should be as follows:
This ugly code is the result of transpiling and bundling the
StoryEditor component. As you can see, it asynchronously loaded children components too, namely
There is, however, some restrictions to this approach. The
require.ensure method resolves modules statically. It means that you need to specify the modules in string literals, that are evaluated at compile time, so you can't use variables. But, if you want to lazy load modules dynamically, ECMAScript's dynamic
import() will have the pleasure to satisfy your request.
The dynamic import is a pretty awesome feature ECMAScript came up with. It offers the possibility to handle cases like: computed module specifiers, conditional loading of modules, accessing exports and default exports, and many more. The dynamic import proposal is in stage 3 at the time of this writing.
import() relies on
Promise. This implies that you have to use some polyfills like es6-promise or promise-polyfill in order to make it work. You're gonna need
babel support too, using the Syntax Dynamic Import plugin that allows the parsing of
Here is the
import() version of the previous example:
require.ensure example, we've seen how it loads a statically resolved modules. Now, what if
Editor.jsx doesn't know which editor to load? What if we give it an array of editors, so it can load them? Let's see how
import() handles this like a boss:
import() statement is dynamic. Yes! But it needs something to rely on: a context. In our case, this context is the
./editors/ that we feed it.
At compile time, ECMAScript cannot resolve the
module argument. It's going to systematically ignore it, and take the first static piece of the module name
./editors/, then generate a context module using it.
Wait, what the heck is a context module?
A context module is a kind of bundle that Webpack generates for a given directory, in order to make it possible to dynamically load any file in that directory. Take for example Webpack's
We just created a context module that contains the 2 files
MessageEditor.jsx. Now, we can dynamically load them by simply
Notice: the context returned from
require.context is a function that works like a local
require, and in the same time an object that contains the paths to all the files it holds.
Here is what Webpack says about
A context module is generated. It contains references to all modules in that directory that can be required with a request matching the regular expression. The context module contains a map which translates requests to module ids.
The context module also contains some runtime logic to access the map.
Okay, but what about asynchronous routing?
Example of asynchronous routing
react-router we will define some routes in our app in order to load the components of those routes asynchronously:
Here are some classic routes:
The components are loaded synchronously because we're importing them statically. To do it dynamically we need to use a wrapper component that loads the other components —
StoryPage. The wrapper component uses the
componentDidMount lifecycle method to load those components.
We are going to write it as a factory function that takes a
name argument in order to know what to load.
This factory returns a component class which the router renders in the page, and once it is mounted, the class imports the real component (
StoryPage) and renders it. The following explains how we should use it:
Now, all you need to do is visit those routes, so that you can appreciate how amazing asynchronous import of components is. In the case of the
/list route, React's representation of the components tree should look like this:
Optimizing production performances is a boundless topic. There are many other strategies that help improving it. Thus it should be clear that Asynchronicity is merely one solution amongst other various ones that can be used to enhance production performances. I hope this post was useful and could enlighten some curious minds about ECMAScript's asynchronous loading.
Thanks for reading.