Using JavaScript modules in production: current state of affairs. Part 2

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'; //         .   //    ,       . dynamicImportPolyfill.initialize({modulePath: '/modules/'});
      
      





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) {    //         .    const modulepreloadMap = {};    for (const [fileName, chunkInfo] of Object.entries(bundle)) {      if (chunkInfo.isEntry || chunkInfo.isDynamicEntry) {        modulepreloadMap[chunkInfo.name] = [fileName, ...chunkInfo.imports];      }    }    //  -   ...    console.log(modulepreloadMap);  } }
      
      





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:





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:





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?






All Articles