The original address: philipwalton.com/articles/us… The author of this article is PHILIP WALTON. The author of this article is PHILIP WALTON
Two years ago, I wrote about the Module/Nomodule technology, which allows you to use a packer and a converter to generate two versions of your code base when writing ES2015+ code, A version with modern syntax (loaded via
But back then, even though it was possible to deploy modern JavaScript in production and most browsers supported modules, I still recommended packaging your code.
Why is that? Mainly because I felt it was slow to load modules in the browser. Although new protocols like HTTP/2 theoretically efficiently supported loading large amounts of small files, all performance studies at the time suggested that using a packer was more efficient.
In fact, the research was incomplete. The module test sample used by the institute consists of unoptimized and unminiaturized source files deployed to production. It does not compare the optimized module package with the optimized original script.
However, there was no better way to deploy modules at the time. However, some recent advances in packaging technology allow production code to be deployed as ES2015 modules (both static and dynamic imports) to achieve better performance than non-modules. In fact, this site has been using native modules in production for several months now.
Misunderstanding of modules
Many of the people I talk to think of modules as an application of choice in mass production environments. Many of them cited the research I just mentioned and advised against using modules in a production environment unless:
. Small Web applications, with less than 100 modules in total, have relatively shallow dependency trees (i.e., a maximum depth of less than 5).
If you’ve ever looked in the node_modules directory, you probably know that even small applications can easily have more than 100 module dependencies. Let’s take a look at how many module dependencies some popular toolkits on NPM have:
package | The module number |
---|---|
date-fns | 729 |
lodash-es | 643 |
rxjs | 226 |
The main misconception about modules is that there are only two options when using modules in a production environment :(1) deploy all source code as is (including the node_modules directory), and (2) don’t use modules at all.
If you consider the advice given by the study I cited, it doesn’t say that loading modules is slower than normal loading scripts, nor does it say that you shouldn’t use modules. It just says that if you deploy hundreds of uncompressed module files into production, Chrome won’t be able to load them as quickly as a single compressed module. Therefore, it is advisable to continue using packers, compilers, and compressors.
The reality is that you can use all of the above techniques in production and use the ES2015 module at the same time!
In fact, because browsers already know how to load modules (and can degrade browsers that don’t support modules), modules are the format we should be packaging. If you examine the output code generated by most popular packers, you’ll find a lot of boilerplate code. Code for runtime in rollup and Webpack), whose sole purpose is to dynamically load other code and manage dependencies, but if we only use modules with import and export statements, we don’t need this code!
Fortunately, at least one popular Rollup today supports modules as an output format, which means you can package code and deploy modules in production (without loader boilerplate code). Because Rollup (which, in my experience, is the best packer) is tree-shaking, Rollup packages modules with the least code of any of the packager output modules to date.
Update: Parcel plans to add module support in the next release. Webpack does not currently support module output formats, but there are some relevant discussions # 2933, # 8895, # 8896.
However, Rollup has a plug-in (rollup-plugin-Commonjs) that converts commonJS source code to ES2015. It would certainly be better if your dependencies were managed using ES2015 modules in the first place, but having dependencies that are not managed that way does not stop you from deploying your modules.
In the rest of this article, I’ll show you how to package into modules (including granularity using dynamic imports and code splitting), explain why it’s often more efficient than raw scripts, and show you how to handle browsers that don’t support modules.
Optimal packaging strategy
Packaging production code is always a trade-off. On the one hand, you want your code to load and execute as quickly as possible. On the other hand, you don’t want to load code that users don’t actually need.
You also want your code cached as much as possible. One big problem with packaging is that changes to even one line of code invalidate the entire packaged package cache. If you deploy your application directly using ES2015 modules, as they do in source code, you are free to make small changes while leaving most of your application’s code in the cache. But as I’ve already pointed out, it also means that your code takes longer to load by a new user’s browser.
Therefore, the challenge in finding the optimal packaging granularity is to strike the right balance between load performance and long-term caching.
By default, most packers split code with dynamic imports, but I don’t think dynamic import-only code splitting is granular enough, especially for sites with a large number of retained users (caching is important).
In my opinion, you should break up your code as fine-grained as possible until you start to significantly affect load performance. While I highly recommend doing your own analysis, a look at the studies cited above can lead to a general conclusion. There is no significant performance difference when fewer than 100 modules are loaded. Studies of HTTP/2 performance found no significant difference when loading less than 50 files (although they only tested 1, 6, 50, and 1000, so 100 files would probably do).
So what’s the best way to break up code? In addition to splitting code through dynamic imports, I recommend splitting code at NPM package granularity, where modules in node_modules are merged into files named after their package names.
Package level code splitting
As mentioned above, some recent advances in packaging technology have made high-performance module deployment possible. The enhancements I mentioned refer to the two new features of Rollup: automatic code splitting via dynamic import() (added in V1.0.0) and programmable manual code splitting via the manualChunks option (added in V1.11.0).
With these two capabilities, it is now easy to configure the build for code splitting at the package level.
This is an example configuration using the manualChunks option. Each module in the node_module will be merged into a file named with the package name (of course, node_modules must be in the module path).
export default {
input: {
main: 'src/main.mjs',
},
output: {
dir: 'build',
format: 'esm',
entryFileNames: '[name].[hash].mjs',
},
manualChunks(id) {
if (id.includes('node_modules'// Return the directory name following the last 'node_modules'. // Return the directory name following the last' node_modules' // Usually this is The package, but it could also be the scopedirs = id.split(path.sep);
return dirs[dirs.lastIndexOf('node_modules') + 1); }}},Copy the code
The manualChunks option receives a function that takes the module file path as its only parameter, or it can return a filename to which the module will be added. If nothing is returned, the module in the argument is added to the default file.
Consider an application that imports the cloneDeep(), Debounce (), and find() modules from the Lodash-es package. The above configuration will put each module (and any other Lodash modules they import) together into an output file called npm.lodash-es.xxxx.mjs (where XXXX is the hash of the Lodash-es module file).
At the end of the file, you’ll see export statements like this (note that it only contains export statements for modules added to the block, not all Lodash modules):
export {cloneDeep, debounce, find};
Copy the code
Hopefully this example makes it clear how manually breaking code using Rollup works. Personally, I find code splitting using import and export statements easier to read and understand than code splitting using non-standard, packager-specific implementations.
For example, it’s hard to keep track of what’s going on in this file (I used Webpack to do the actual output of a broken code for a project), and it’s not really needed in a mode-enabled browser:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["import1"] and {/ * * * /"tLzr": / *! * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./app/scripts/import-1.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / / *! exports provided: import1 */ /***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "import1".function() { return import1; });
/* harmony import */ var _dep_1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./dep-1 */ "6xPP");
const import1 = "imported: " + _dep_1__WEBPACK_IMPORTED_MODULE_0__["dep1"]; / * * * /}}));Copy the code
What if you have hundreds of NPM dependencies?
As I said above, I think code splitting at the package level is the best state of site code splitting without being too aggressive.
Of course, if your application imports modules from hundreds of different NPM packages, the browser may not be able to load them all efficiently.
However, if you do have a lot of NPM dependencies, don’t abandon this strategy entirely just yet. Keep in mind that you may not load all NPM dependencies on every page, so it’s important to check how many dependencies are actually loaded.
However, it is true that some very large applications have so many NPM dependencies that they cannot realistically split the code for each and every one of them. If this is your case, I suggest you find a way to group some dependencies into a public file. In general, you can group packages that might change at the same time (for example, React and react-dom) because they must fail together (for example, the example application I’ll show you later groups all React dependencies into the same file).
Dynamic import
One disadvantage of using native import statements for code splitting and module loading is that it requires developers to do compatibility processing for browsers that do not support modules.
If you want to lazily load code with dynamic import(), you’ll also have to deal with the fact that some browsers support modules but don’t support dynamic import() (Edge 16-18, Firefox 60-66, Safari 11, Chrome 61-63).
Fortunately, a very small (~400 bytes), very high-performance polyfill is available for dynamic import().
Adding polyfills to your site is easy. All you have to do is import it and initialize it at the main entry point of the application (before calling import()):
import dynamicImportPolyfill from 'dynamic-import-polyfill';
// This needs to be done before any dynamic imports are used. And if your
// modules are hosted in a sub-directory, the path must be specified here.
dynamicImportPolyfill.initialize({modulePath: '/modules/'});
Copy the code
The last thing to do is tell a Rollup dynamic output code in the import () renamed you specify another name (through the output. DynamicImportFunction option configuration). Dynamically importing Polyfill uses the name __import__ by default, but it can be configured.
The reason you need to rename the import() statement is because import is a keyword in JavaScript. This means that it is impossible to populate native import() with the same name, because doing so would result in syntax errors.
It’s good to have Rollup rename it at build time, which means your source code can use the standard version, and you won’t have to change it again in the future when polyfill is no longer needed.
Load JavaScript modules efficiently
When you use code splitting, it is a good idea to preload all modules to be used immediately (i.e., all modules in the main entry module import diagram).
However, when you load actual JavaScript modules (via
<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"> <! -... --> <scripttype="module" src="/modules/main.XXXX.mjs"></script>
Copy the code
In fact, for preloaded native modules, ModulePreload is actually much stricter than traditional Preload in that it not only downloads files, but also immediately starts parsing and compiling them outside of the main thread. Traditional preloading cannot do this because it does not know whether the file will be used as a module script or raw script at preloading time.
This means that loading modules via ModulePreload is usually faster and is less likely to cause the main thread to stall when instantiated.
generatemodulepreload
The list of
Each entry file in Rollup’s Bundle object contains a complete list of imports in its static dependency diagram, so it’s easy to get a list of which files need to be preloaded in Rollup’s generateBundle hook.
While some ModulePreload plug-ins do exist on NPM, it only takes a few lines of code to generate a modulePreload list for each entry point in the diagram, so I prefer to create it manually like this:
{
generateBundle(options, bundle) {
// A mapping of entry chunk names to their full dependency list.
const modulepreloadMap = {};
for (const [fileName, chunkInfo] of Object.entries(bundle)) {
if(chunkInfo.isEntry || chunkInfo.isDynamicEntry) { modulepreloadMap[chunkInfo.name] = [fileName, ...chunkInfo.imports]; } } // Do something with the mapping... console.log(modulepreloadMap); }}Copy the code
For example, here’s how I generated the ModulePreload list for this site and my demo application.
Note: While ModulePreLoad is definitely better than the original Preload for module scripts, it has worse browser support (currently chrome only). If a significant portion of your traffic is non-Chrome, it makes sense to use Classic Preload. Unlike using ModulePreload, one thing to note when using Preload is that the preload script is not placed in the browser’s module map, which means that the preloaded request may be processed more than once (for example, if the module imports the file before the browser has finished preloading).
Why deploy native modules?
If you’re already using a packer like WebPack, and you’re already using fine-grained code to split and preload these files (similar to what I describe here), then you might be wondering if it’s worth changing tack and using native modules. Here are a few reasons WHY I think you should consider it, and why packaging into native modules is better than using raw scripts with module-loaded code.
Smaller code totals
When using native modules, modern browsers do not have to load any unnecessary module loading or dependency management code for the user. For example, if you use native modules, you don’t need the WebPack runtime and manifest at all.
Better preloading
As I mentioned in the previous section, using modulePreload allows you to load code and parse/compile it outside of the main thread. All other things being equal, this means that pages interact faster and the main thread is less likely to be blocked during user interaction.
Therefore, no matter how fine-grained you are in code splitting your application, loading modules using import statements and modulePreload is more efficient than loading modules through raw script tags and regular preload (especially if these tags are dynamically generated and added to the DOM at run time).
In other words, the 20 module files packaged by Rollup will load faster than the 20 original script files packaged by Webpack (not because of Webpack, but because it is not a native module).
More future-oriented
Many of the most exciting new browser features are built on modules, not raw scripts. This means that if you want to use any of these features, your code needs to be deployed as a native module, not converted to ES5 and loaded via the original Script tag (a problem I mentioned when trying to use the experimental KV storage API).
Here are some of the most exciting new module-only features:
- Built-in module
- HTML module
- CSS module
- JSON module
- Import the map
- Modules are shared between workers, Service Workers, and Window
Support for older browsers
Globally, more than 83% of browsers natively support JavaScript modules (including dynamic imports), so for most of your users, no processing is required to use this technology.
For browsers that support modules but do not support dynamic imports, you can use the dynamic-import-polyfill mentioned above. Because polyfill is very small and will use the browser’s native dynamic import() when available, there is little size or performance cost to adding this polyfill.
For browsers that don’t support modules at all, you can use the module/nomodule technique I mentioned earlier.
A practical example
Since it’s always easier to talk about cross-browser compatibility than to actually implement it, I built a demo application that uses all the techniques I’ve outlined here.
The demo runs in browsers that do not support dynamic import() (such as Edge 18 and Firefox ESR) or in browsers that do not support modules (such as Internet Explorer 11).
To illustrate that this strategy applies to more than simple use cases, I’ve included many of the features needed for today’s complex JavaScript applications:
- Babel conversion (including JSX)
- CommonJS dependencies (e.g. React, react-dom)
- CSS dependencies
- Asset hashing
- Resolution of the code
- Dynamic import (with Polyfill demotion)
- Module /nomodule Degradation mechanism
The code is hosted on GitHub (so you can derive the repo and build it yourself), and the demo is hosted on Glitch, so you can recombine the code and use the features.
It’s important to look at the Rollup configuration used in the example, because it defines how the final module is generated.
conclusion
Hopefully, this article has convinced you that it is now possible not only to deploy native JavaScript modules in a production environment, but that doing so can improve the load and run time performance of your site.
Here is a summary of the steps needed to do this quickly:
- Use a packer, but make sure the output format is ES2015 module
- Actively split code (down to node packages if possible)
- Preloads all modules in the static dependency diagram (through
modulepreload
) - Use polyfill to support unsupported dynamics
import()
The browser - use
<script nomodule>
Support browsers that do not support modules at all
If you’re already using Rollup in your build setup, I encourage you to try out the techniques described here and deploy native modules (with code splitting and dynamic imports) in production. If you do, please let me know how it went, because I want to hear both your problems and your success stories!
Modules are the clear future of JavaScript, and I hope that all of our tools and dependencies include modules soon. I hope this paper can play a role in promoting this direction.
1. A translation of the author’s previous article: jdc.jd.com/archives/49… 2. Another article about JavaScript native module: www.jianshu.com/p/9aae3884b…
If you think this article is valuable to you, please like it and follow our official website and WecTeam: