Webpack and Dynamic Imports: Doing it Right

Rubens Pinheiro Gonçalves Cavalcante
Frontend Weekly
Published in
5 min readMay 4, 2018
Blocks — Picture under Creative Commons License (source: https://flic.kr/p/6PkwK5)

Webpack and dynamic import

In old versions of Webpack (v1), we commonly used the AMD “require” or the specific Webpack “require.ensure” to dynamic load modules. But for this article, I’m going to use the proposed ES2015 dynamic imports supported by Webpack, since the v2, through a babel plugin and the extra specific Webpack features for it.

Update: If you’re using Babel 7.5+ it already includes the dynamic import plugin for you ;)

Dynamic imports: the basics

The syntax is pretty simple. With the above ES proposal the keyword import gets more power and turns also into a function which returns a Promise:

import("module/foo").then(foo => console.log(foo.default))

The above code will load the foo module at runtime, and resolving it, will log the default export of the module. As the import is a function receiving a string, we can do powerful things like loading modules using expressions.

Dynamic module loading with expressions

Let’s suppose you have an app that has different behavior and visuals in some features for mobile to desktop. In this case, having only a responsive design doesn’t cover what you want, so you build a page renderer which loads and renders the page based on the user platform. It basically uses a strategy pattern that chooses which module should be loaded on runtime. As a smart developer, you don’t want to load the entire code for desktop if the user is on mobile, and vice versa. Let’s check it on the code below:

🤓 — “But hey, this is a pretty simplist approach. Real-world apps don’t have only one page at all! I have a component repository with a lot of pages in my app!”

True, even if we’re dynamic loading the components, this stills a pretty attached solution. Let’s refactor our function:

🤓- “Still not good! My app is made to be accessible from a lot of specific platforms like mobile, desktop, tablet, VR and can be even more in the future!”

Ok, ok. Let’s refactor again:

Now in this example, we’re taking a more functional approach. Created and exported a composite function to do the work, which is able to load for any platform we want using expressions, plus we already exposed two loaders, one for desktop and other for mobile.

Problems while loading files

In Webpack normally we load images as modules using the file loader. The file loader will basically map the emitted file path inside a module.

import(`assets/images/${imageName}.jpg`).then( src => ... )

The problem is if you want to dynamically load a file, in this case, an image, Webpack by default generate a chunk for that module, something similar to this:

The big issue with that is when you request dynamic imported images, it will do a network request to get the chunk and then another one to get the image, adding unnecessary overhead to your app. If you’re using HTTPS is even worse!

🤓 — “Ok, I do this for a lot of images, this turned into a big problem and because of this extra requests, the images are slower to load. How to solve this problem?”

Webpack “magic comments”

Webpack adds a really nice feature to the dynamic imports, the magic comments. With that, you can add some metadata, readable for Webpack, in a way that you can choose the strategy on how Webpack generates and loads the chunks.

Webpack Mode

To solve the problem of dynamic loading files, we can simply choose the loading strategy:

import(/* webpackMode: "eager" */ `assets/images/${imageName}.jpg`)

This will force Webpack to include the file chunk inside the parent bundle/chunk, forcing it to not create a separated chunk for that. This way, all the file paths will be promptly available when your app loads the parent bundle/chunk.

There are four different methods (lazy, lazy-once, eager, weak). You can take a look into the descriptions in more detail here.

Webpack Chunk Name

🤓 — “Hey, I noticed that Webpack just put numbers to generated chunks. This makes debugging harder, as I don’t know if one specific chunk was loaded or not!”

As we can control the loading strategy, we can also use the magic comments to control the generated chunk names too by simply doing this:

import(/* webpackChunkName: "foo-image" */ "assets/images/foo.jpg");
import(/* webpackChunkName: "bar-module" */ "modules/bar");

Instead of numbers, Webpack will use the chosen names to the generated chunks.

Prefetch/Preload

Note: This feature was added on Webpack v4.6

If you’re using HTTP2 is better to break the big bundles in smaller pieces. So, is better to preload that small image chunks than add it to the bigger bundle/chunk right?

To do so, we can simply use, instead of webpackMode: “eager” the webpackPrefetch: true which makes the browser download the chunks after the parent bundle/chunk.

🤓 — “But what is the difference between prefetch and preload?”

Webpack docs explain in more details:

- A preloaded chunk starts loading in parallel to the parent chunk. A prefetched chunk starts after the parent chunk finish.

- A preloaded chunk has medium priority and instantly downloaded. A prefetched chunk is downloaded in browser idle time.

- A preloaded chunk should be instantly requested by the parent chunk. A prefetched chunk can be used anytime in the future.

- Browser support is different.

As prefetch makes the chunk be loaded on the idle time, you can add numbers as the parameter to say to Webpack what is the priority of each one:

// 0 is same as true
import(/* webpackPrefetch: 0 */ "assets/images/foo.jpg");
// this loads first as 1 > 0 (or true)
import(/* webpackPrefetch: 1 */ "modules/bar");
// this one will be the last!
import(/* webpackPrefetch: -100 */ "modules/slowpoke");

The bar.js module has a higher priority to load, so it will be prefetched before foo.jpg and slowpoke.js will be the last one(priority -100).

Going deeper on Code Splitting

If you want to check the “how-to” make a lazy-loaded single page application (SPA) using the discussed dynamic import, you can check out two of my previous articles on this subject. Although the articles use React and React+Redux on the examples, you can apply the same very idea in any SPA based framework/library:

Conclusion

Code splitting is a powerful thing to make your application faster, smartly loading the dependencies on the run. But as Uncle Ben once said:

Know how the tool works in essential to use its maximum performance, and I hope I helped you to know a little more about it now!

--

--