August 20, 2018











In the first article of this Webpack 4 course series, we’ve covered imports and exports. This time we will dig deeper into dynamic imports because they deserve a separate article. We will explain what they are and how to use them. Let’s start!

Webpack 4 course: what are dynamic imports?

Back in the days, ECMAScript modules were completely static. You did have to specify what you wanted to import and export explicitly before running the code. With the dynamic imports proposal, we got an additional, dynamic way to import modules. It is now in stage 3 of the TC39 process and thanks to it, you can dynamically load modules. You might want to do that based on who your user is and what he is doing in your application. For example, you can load code of some sub-page of your Single Page Application only if the user decides to open it. This can save you quite a lot of time during the initial page load.

Using dynamic imports

Dynamic imports operator is used as a function. It takes one parameter that is a string and returns a promise. Once the module is loaded, the promise gets resolved.

If you would like to know more about promises check out Explaining promises and callbacks while implementing a sorting algorithm

1 2 3 4 5 6 7 8 9 document . addEventListener ( "DOMContentLoaded" , ( ) = > { const button = document . querySelector ( '#divideButton' ) ; button . addEventListener ( 'click' , ( ) = > { import ( './utilities/divide' ) . then ( divideModule = > { console . log ( divideModule . divide ( 6 , 3 ) ) ; // 2 } ) } ) ; } ) ;

If you open the network tab in the developer tools you can see that as soon as you click the button the module gets downloaded and not sooner. It is worth noticing that if you click the button again, the file containing the divide module does not get downloaded again.

Using dynamic imports with webpack will cause a new chunk to be created that will be considered an async chunk.

Async chunks were mentioned in the fourth part of the course: Code splitting with SplitChunksPlugin

Such chunk will be bundled into a separate file. You need to watch out though when using an expression to create a path to the file. Consider this example:

1 2 3 4 5 6 7 8 9 10 11 12 let fileName = '' ; document . addEventListener ( "DOMContentLoaded" , ( ) = > { const button = document . querySelector ( '#divideButton' ) ; fileName = 'divide' ; button . addEventListener ( 'click' , ( ) = > { import ( ` . / utilities / $ { fileName } ` ) . then ( divideModule = > { console . log ( divideModule . divide ( 6 , 3 ) ) ; // 2 } ) } ) ; } ) ;

After building your project with that code you will discover that webpack created distinct async chunks for every module in the utilities directory. This is because webpack can’t know during the compilation what modules will be imported.

You also need to know that fully dynamic statements such as import(pathToFile) will not work because webpack requires at least some file location information. This is because pathToFile can be any path to any file in your project and webpack will create async chunks from every module in the pointed directory. You can customize that behavior, which we will do right now.

Using magic comments with webpack

Specification of import does not allow you to use any other arguments than the file name. Luckily, with webpack, you can do it with so-called magic comments.

webpackInclude and webpackExclude

In the previous section, we’ve mentioned that webpack will create async chunks for every module in the directory that we are pointing to. While this is a default behavior, it can be changed.

One way to do it is through webpackExclude, which is a regular expression that will be matched against potential files that could be imported. Any matching module will not be bundled.

1 2 3 4 import ( ` . / utilities / $ { fileName } ` /* webpackExclude: /subtract.js$/ */ )

With the code above, the file subtract.js will not be bundled, even if it is in the utilities directory.

If you would like to know more about regular expressions, check out my regex course

The opposite argument is called webpackInclude. If you use it, only modules that match the regular expression provided will be bundled.

webpackMode

The property webpackMode defines one of the modes for resolving dynamic imports. The following ones are supported:

lazy

This is a default mode. It results in async chunk being generated for each module dynamically imported.

lazy-once

If used, it causes generation of a single async chunk for all modules that match the import call.

1 2 3 4 5 6 7 import ( ` . / utilities / $ { fileName } ` /* webpackMode: "lazy-once" */ ) . then ( divideModule = > { console . log ( divideModule . divide ( 6 , 3 ) ) ; // 2 } )

With this code, webpack will create one async chunk for all modules in the utilities directory. It would result in user downloading all modules as one file.

eager

Prevents webpack from generating extra chunks. All imported modules are included in the current chunks, so no additional network requests are made. The promise is still returned, but it gets automatically resolved. Dynamic import with the mode eager differs from a static import in a way that the module isn’t executed at all until the import() call is made.

weak

Prevents the additional network requests to be made at all. Promise will be resolved only if the module has already bee loaded in some other way. Otherwise, the promise is rejected.

webpackChunkName

It is a name for the new chunk. It can be used combined with the [index] and [request] variables.

The [index] is an index of a file in the current dynamic import statement. On the other hand, [request] is the dynamic part of your imported file path.

1 2 3 4 import ( ` . / utilities / $ { fileName } ` /* webpackChunkName: "utilities-[index]-[request]" */ )

An example of a filename generated from the code above is utilities-0-divide.js

Please note that in situations in which there is surely just one async chunk generated (such as not generating the path dynamically at all, or using the lazy-once mode), [index] and [request] can’t be used.

Increasing performance with prefetching and preloading

With the webpack 4.6.0, we were given a support for prefetching and preloading. Using these directives will change the way that browsers handle your async chunks.

Prefetch

By using prefetch you indicate that the module is probably needed in the future. The browser will download it in the idle time and the downloading will start after the parent chunk finishes.

1 2 3 4 5 import ( ` . / utilities / divide ` /* webpackPrefetch: true */ /* webpackChunkName: "utilities" */ )

Such import will result in <link rel="prefetch" as="script" href="utilities.js"> being added to the head of the page. As a result, the browser will prefetch the file in the idle time.

Preload

By marking the resource with preload you indicate that it will be needed right now. The async chunk will start loading in parallel to the parent chunk. If the parent chunk is loaded first, the page can already be displayed while waiting for the async chunk to load. This might give you a performance boost.

1 2 3 4 5 import ( ` . / utilities / divide ` /* webpackPreload: true */ /* webpackChunkName: "utilities" */ )

The code above will result in <link rel="preload" as="script" href="utilities.js"> being used. Using wepbackPreload incorrectly can hurt your performance, so watch out and use it carefully.

Summary

This time we’ve learned how to increase the performance of our application with the dynamic imports. They can definitely decrease the initial page load time of your application. With additional arguments that you can pass to webpack by comments, you can customize it even more and add prefetch and preload support. All of that will increase the experience of your users and make the site feel more dynamic.