Webpack From Zero to Hero - Chapter 4: Dynamic Imports and Code Splitting
source link: https://www.tuicool.com/articles/777nMnr
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Webpack From Zero to Hero
Chapter 4:Dynamic Imports and Code Splitting
This article is part of the Webpack from Zero to Hero series , for more background or for the index you can check theChapter 0: History.
Previous - Chapter 3: Everything is a Module .
Next - Chapter 5: Route Based Code Splitting with React (coming soon).
Introduction
S ometimes you want to make your app more flexible, but responsive design is not enough and your UI needs not only a different look and feel but another behavior on different platforms.
You may want to swap business logic at runtime , depending on your user actions. Or your app became too big and you may just want to load things only when necessary.
For all this, we have a solution, and the most important thing is an ECMA proposal !
Webpack supports it (of course with some differences, same as with the official static import) and adds a nice behavior to it: code splitting.
How it works
The syntax is pretty straightforward, we have a function that returns a Promise.
Let’s say we have a dumb module called module-1.js
:
To import it dynamically we do something like this:
But there’s a problem if you try to run it:
Enabling Dynamic Imports on Babel
The problem is because dynamic imports is only an ECMA proposal , there’s a good chance that this will be a reserved syntax of JavaScript, so BabelJS will “complain” when parsing it. In fact, if you remove the babel-loader rule, you can see that Webpack works fine with it:
But hey, if you look up the error, Babel gives you this tip :
“Add @babel/plugin-syntax-dynamic-import (https://git.io/vb4Sv) to the ‘plugins’ section of your Babel config to enable parsing.”
So let’s install it:
yarn add @babel/plugin-syntax-dynamic-import --dev
…and enable it in .babelrc
:
Enabling babel-loader
rule again and running yarn start:dev
, now all works fine!
What Happens to the Bundle?
If you check the build logs, you’re going to see this:
That 1.js
means Webpack created a “chunk” and included our dynamic imported module inside of it.
If you run the dev server and put a breakpoint right on the `import()` you will not see the 1.js
on the network tab, but as soon you release the breakpoint, the request to fetch it is done.
Visualizing it
When the project grows, you probably will find it necessary to check how your bundle ended up, and how to optimize it: “ what if I do this tweak ?”. But not everyone is a Hackerman and is able to read this output extracting all the information from it.
However, Webpack has plenty of options for visualizing the final output and making it more readable. The one that I like most is Webpack Bundle Analyser . Let’s check how it works. First, install it:
yarn add webpack-bundle-analyzer --dev
Add this to the npm scripts on package.json
:
There are two things happening here:
- Command composition: the
analyse
script will result in:
webpack --mode=production --env.analyse
- We’re giving an environment variable to Webpack, and we’ll be able to access it within the configuration.
Now, let’s setup our webpack.config.js
and use the given --env.analyse
parameter on it:
Now just run the npm script and the bundle analyzer will automatically open a new tab in your default browser:
yarn analyse
You’re going to see this:
Loading Using Expressions
So now we can see that Webpack splits your code by default when you do a dynamic import, but let’s go further and play around it.
Let’s create a new module called module-2.js
which exports only this function:
You can see that there’s a pattern for our module names, where every module is marked by its number. This way it is possible to use an expression to import them:
This will output the default functions of both modules 1 and 2 as a result:
This function does nothing and I’m totally useless! Deal with it! :sunglasses:
And if you analyze the bundle, you will see that Webpack automatically creates a context based on your expression :
You see the power of functional programming — we’re basically transforming a set of numbers to their respective module default export function outputs! As long our modules keep the same signature we can play around like this.
This opens a lot of new possibilities, for example as we said before — loading UI components depending on the platform (or browser), taking different actions after the user chooses or inputs some value into your app, etc.
Lazy Loading
Most of the time you will want to defer the module loading so that you could use the module only when it is necessary. There are some techniques to do so, let’s check them. :eyes:
Event-based loading
Let’s create a file called lazy-one.js
that exports a string:
Now at the end of index.js,
paste this snippet:
We have just appended a button with a dynamic import on the “click” event.
Loading the page and looking at the network tab shows us that it loads the page, and we can see it requesting the two chunks from before:
And then, after clicking the button:
Webpack “Magic Comments”
As Webpack adds all this behavior to dynamic imports, it would be nice if we had some control over it, right?
But we do! :tada:
Webpack provides a nice way to configure these imports through comments following a certain pattern.
Chunk name
:confused: — “Hey, but the name 2.js
isn’t really helping to know what we have inside it, can we rename it?”
Yep! Let’s name it after myAwesomeLazyModule :
This will result in:
Preload Module
Let’s say our lazy-module is super important and we don’t want a delay on the button content change after the user clicks it.
We can choose to preload this resource as we’re pretty sure it will be used on the current page:
This will make webpack add a `<link rel=”preload”>` at the start of the page, making this resource to be loaded (but not executed) before the `main.js`.
This is pretty useful for small resources where users have a `high latency network`, causing a big delay on the execution of small chunks not because of it’s size, but because of the HTTP protocol process (being even worse on HTTPS because of the “handshake”).
“This will give a little load time boost since it only needs one round-trip instead of two.” — Webpack Docs
Prefetch Module
:confounded: — “What!? What’s the difference between preload and prefetch ?
It looks similar, but prefetch will not load before the bundle/chunk that requests this resource, it will load after on the browser idle time .
This makes it perfect for resources that will very probably be used in future . For instance, you have the data (through analytics) that the majority of unsubscribed users who visit your “products” page will with a high probability access the “subscribe” page. Since you’re a smart developer, you set a prefetch import on “products”, making the browser fetch and cache this resource in its spare time:
As you can see, it works similar to the preload, activating it just by adding the “magic comment” above.
And here is a tip (works for preload too): if you have multiple prefetch/preload resources, you can define their priority:
<em>/* webpackPrefetch:</em><strong><em> 42</em></strong><em> */</em>
Chunk Mode
You may be asking yourself:
I have a 100 of those modules-${id}
and as I’m requesting them on the page directly, I don’t want to request 100 separate files, since most of my users will suffer because of high latency .
For that we also can control how Webpack resolves and splits the chunks for dynamic imports, using the webpackMode magic comment. From the docs :
“lazy” (default) : Generates a lazy-loadable chunk for each import()ed module.
“lazy-once” : Generates a single lazy-loadable chunk that can satisfy all calls to import(). The chunk will be fetched on the first call to import(), and subsequent calls to import() will use the same network response. Note that this only makes sense in the case of a partially dynamic statement, e.g. import(`./locales/${language}.json`), where there are multiple module paths that could potentially be requested.
“eager” : Generates no extra chunk. All modules are included in the current chunk and no additional network requests are made. A Promise is still returned but is already resolved. In contrast to a static import, the module isn’t executed until the call to import() is made.
“weak” : Tries to load the module if the module function has already been loaded in some other way (i. e. another chunk imported it or a script containing the module was loaded). A Promise is still returned but, only successfully resolves if the chunks are already on the client. If the module is not available, the Promise is rejected. A network request will never be performed. This is useful for universal rendering when required chunks are always manually served in initial requests (embedded within the page), but not in cases where app navigation will trigger an import not initially served.
Let’s try out lazy-once
on our dynamic import with an expression:
And we see the result:
You see, all chunks which were separate are now in the same common chunk, making it easy for the application, as it loads all modules with one single request.
If instead, we use the “ eager ”, all these modules will be included inside the one which is importing them, in our case main.js
.
W rapping up, in this chapter we went deep into Webpack and used the dynamic-imports, tweaking the way the code is being split in the process (while visualizing everything with the bundle analyzer).
We’re almost done and the only thing left to us is to take advantage of what we learned in this chapter while using React and the new lazy/Suspense API. See you in the next chapter!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK