Today we publish the second part of the translation of the material, which is devoted to the use of JS-modules in production.
β By the way, here is the
first part of the article.
Dynamic import
One of the minuses of using real import expressions to separate code and load modules is that the task of working with browsers that do not support modules rests with the developer.
And if you want to use dynamic
import()
commands to organize lazy code loading, then you, among other things, will have to deal with the fact that some browsers, although, most certainly,
support modules , still do
not support dynamic import () commands (Edge 16β18, Firefox 60β66, Safari 11, Chrome 61β63).
Fortunately, this problem will help us solve a small (about 400 bytes in size) and extremely fast
polyfill .
Adding this polyfill to a web project is very simple. You just need to import it and initialize it at the main entry point to the application (before calling any of the
import()
commands in the code):
import dynamicImportPolyfill from 'dynamic-import-polyfill';
And the last thing that needs to be done in order for this scheme to work, is to tell Rollup that it needs to rename the dynamic
import()
commands that appear in the code using the name you
choose (via the
output.dynamicImportFunction option). A polyfill that implements the dynamic import feature uses the
__import__
name by default, but it can be
customized .
The reason you need to rename
import()
expressions is because
import
is, in JavaScript, a keyword. This means that it is impossible, by polyfill means, to organize the replacement of the standard
import()
command with a command of the same name. If you try to do this, a syntax error will occur.
But itβs very good that Rollup renames the commands during the build of the project, as this means that standard commands can be used in the source code. In addition, in the future, when the polyfill will no longer be needed, the project source code will not have to be rewritten, changing to
import
what was previously named somehow differently.
Efficient JavaScript loading
Whenever you use code separation, it does not hurt to organize preloading of all modules that, as you know, will be loaded very soon (for example, these are all modules in the dependency tree of the main module, which is the entry point to the project).
But when we load real JavaScript modules (via
<script type="module">
and then through the corresponding
import
commands), we must use the
modulepreload attribute instead of the usual
preload , which is intended only for classic scripts.
<link rel="modulepreload" href="/modules/main.XXXX.mjs"> <link rel="modulepreload" href="/modules/npm.pkg-one.XXXX.mjs"> <link rel="modulepreload" href="/modules/npm.pkg-two.XXXX.mjs"> <link rel="modulepreload" href="/modules/npm.pkg-three.XXXX.mjs"> <!-- ... --> <script type="module" src="/modules/main.XXXX.mjs"></script>
In fact,
modulepreload
is definitely better than the traditional
preload
mechanism in preloading real modules. The fact is that when you use it, it is not just that the file is downloaded. In addition, it immediately, outside the main thread, starts parsing and compiling the script. A regular
preload
cannot do this because it, during the preload, does not know whether the file will be used as a module or as a regular script.
This means that loading modules using the
modulepreload
attribute
modulepreload
often faster, and that when modules are initialized, it is less likely to create an excessive load on the main thread, causing interface problems.
Creating a list of modules for preloading
The input fragment in the Rollup
bundle object contains a complete list of imports in their static dependency trees. As a result, in the Rollup
generateBundle hook, itβs easy to get a list of files that need to be preloaded.
Although plugins can be found in npm for generating modulepreload lists, creating a similar list for each input point in the dependency tree requires only a few lines of code. Therefore, I prefer to create such lists manually, using something like this code:
{ generateBundle(options, bundle) {
Here, for example, is how I created a
modulepreload list for
philipwalton.com and for my
demo application , which we'll talk about below.
Please note that although the
modulepreload
attribute
modulepreload
definitely better than the classic
preload
for loading module scripts, it has the worst browser support (currently only Chrome supports it). If a significant portion of your traffic comes not from Chrome, then in your situation it may make sense to use regular
preload
instead of
preload
.
Regarding the use of
preload
, I would like to warn you about something. The fact is that when loading scripts using
preload
, unlike
modulepreload
, these scripts do not get into the browser module map. This means that there is a possibility that preload requests can be executed more than once (for example, if the module imports the file before the browser has finished preloading it).
Why deploy real modules in production?
If you already use a bundler like
webpack , as well as if you already use code splitting and preloading the corresponding files (similar to what I just said), then you may be wondering if you should switch to a strategy focused on the use of real modules. There are several reasons that make me believe that you should consider switching to modules. In addition, I believe that converting a project into real modules is better than using classic scripts with their own code designed to load modules.
β Reducing the total amount of code
If the project uses real modules, then users of modern browsers will not have to download some additional code designed to load modules or to manage dependencies. For example, when using real modules, you will not need to load
the runtime mechanisms and the webpack manifest .
β Improved preload code
As mentioned in the previous section, using the
modulepreload
attribute allows
modulepreload
to load code and
modulepreload
and compile it outside the main thread. Everything else, compared to the
preload
attribute, remains the same. This means that thanks to
modulepreload
pages become interactive faster, and that reduces the likelihood of blocking the main stream during user interaction.
As a result, regardless of what size the application code is divided into fragments, it will be much more productive to load these fragments using the import commands and the
modulepreload
attribute than to load them using the regular
script
tag and the usual
preload
attribute (especially if the corresponding tags are generated dynamically and added to the DOM at runtime).
In other words, a rollup bundle of some project code, consisting of 20 module fragments, will load faster than a bundle of the same project, consisting of 20 classic script fragments prepared by webpack (not because of using webpack, but because that these are not real modules).
β Improving the future focus of code
Many great new features of browsers are built on the basis of modules, and not based on classic scripts. This means that if you plan to use these features, then your code should be presented in the form of real modules. It should not be something transpiled in ES5 and loaded with the means of the classic
script
tag (this is the problem I wrote about when I tried to use the experimental
KV Storage API ).
Here are some of the most interesting new browser features that focus exclusively on modules:
Legacy Browser Support
Globally,
more than 83% of browsers support JavaScript modules (including dynamic import), as a result, most users will be able to work with a project that has switched to modules without any special effort from the developers of this project.
In the case of browsers that support modules but do not support dynamic import, it is recommended to use the
dynamic-import-polyfill polyfill described above. Since it is very small and, if possible, uses the standard browser-based
import()
method, the use of this polyfill has practically no effect on the size or performance of the project.
If we talk about browsers that do not support modules at all, then to organize work with them, you can use the
module / nomodule pattern .
βOperating example
Since it is always easier to talk about cross-browser compatibility than to achieve it, I created a
demo application that uses the technologies discussed above.
This application works in browsers like Edge 18 and Firefox ESR, which do not support dynamic
import()
commands. In addition, it works in browsers like Internet Explorer 11, which do not support modules.
In order to show that the strategy discussed here is suitable not only for simple projects, I used many features in this application that are needed today in large projects:
- Code transformation using Babel (including JSX).
- CommonJS dependencies (e.g. react and react-dom).
- CSS dependencies.
- Resource Hashing
- Code separation
- Dynamic import (with a fallback as a polyfill).
- Implementation of the module / nomodule pattern.
The project code can be found on
GitHub (that is, you can fork the repository and build the project yourself), the demo version is hosted on
Glitch , which allows you to experiment with it.
The most important thing in the demo project is the
Rollup configuration , as it determines how the resulting modules are created.
Summary
I hope this material has convinced you not only of the possibility of deploying standard JavaScript modules in production, but also that it can really improve the site load time and its performance.
Here is a brief overview of the steps that you need to take in order to implement the modules in the project:
- Use a bundler, among the output formats supported by which there are ES2015 modules.
- Aggressively approach code separation (if possible, right up to the allocation of dependencies from
node_modules
into separate fragments). - Preload all the modules that are in your static dependency tree (using
modulepreload
). - Use polyfill for browsers that do not support dynamic
import()
statements. - Use the module / nomodule pattern to organize work with browsers that do not support modules.
If you are already using Rollup to build a project, I would like you to try what I was talking about here and go out to deploy real modules in production (using code separation and dynamic import techniques). If you do,
let me know how you are doing. I am interested to know about problems and about successful cases of module implementation.
Itβs very obvious that modules are the future of JavaScript. I would like to see, and preferably, soon, how the tools and auxiliary libraries we use adopt this technology. I hope this material can at least help this process a little.
Dear readers! Do you use JS modules in production?