As a product evolves, it is often inevitable that the entire application will get bigger and bigger, and the first screen rendering time or first load time will be longer. Then performance tuning is on the agenda. Among many performance optimization schemes, loading modules on demand is undoubtedly one of the most effective methods. Modules are easily loaded on demand using Webpack’s import() method.

Let’s start with a simple example to get a general idea of how WebPack implements on-demand loading of modules.

The simplest example

Step 1: Define a module a.js that needs to be loaded on demand

export default function a() {
  console.log('I'm module A');
}
Copy the code

Step 2: Introduce module A into the main module (index.js)

import('./a').then(({ default: a }) = > {
  console.log(a);
});
Copy the code

Step 3: Start the webpack-dev-server with “NPM run start” (different configurations may vary).Then in the Network layout of the browser, after using JS filter, you will see that two JS files are loaded. One is index.js (main module) and the other is 0.async.js (load file on demand). In this way, modules can be easily loaded on demand, without any special requirements to modify the WebPack configuration. How to use Component loading on demand in ReactUse of the react. Lazy method.

Note: The loaded JS file name varies depending on the webpack configuration.

Our needs are constantly changing, and basic usage methods often fail to meet the imaginations of our product managers. Let’s take a look at some more interesting uses of import() and confidently say “OK, no problem” to the product manager.

Import () usage method

Dynamic file name (path)

The previous examples used static filenames (‘./a’, no variables), but dynamic filenames are often needed in real life. For example, the module loaded by the page is configurable and the configuration result is returned by the server. Fortunately, WebPack supports dynamic file names. Webpack does not support full dynamic file names, which means exceptions will occur when using the following statement.

const path = './a';
import(path).then(({ default: a }) = > {
  console.log(a);
});
Copy the code

Service request could not find ‘./a’ module:The on-demand request file “0.async.js” is missing from the compilation:The reason full dynamic filenames are not supported is that WebPack works by statically scanning files and then processing them according to certain rules. When Webpack detects the “import()” syntax, it converts the variable into the “.* “of the regular expression, matches the file name according to this rule, and prints the independent chunk of the matched file. If the file pathname has only one variable, it matches all files in the directory, which is obviously not reasonable, so Webpack simply prints WARNING and does nothing.

Second, the dynamic file name rule matches the file must be likely to be used, so it is much easier to understand this note, we continue to follow the previous content. If the scanned statement is import(./locale/${language}. Json), the.json file in the ‘./locale/’ directory will be compiled to generate a separate on-demand request file. That is, if the file generated by the match is not used, then it is wasted. So we write the file name and convert the variable to “.* “to match the file must be possible to use.

Configure the compiled file name

Normally there is no configuration required to use an on-demand module, but in some cases we may have requirements for the file names generated by the compilation (for identification purposes, for example), and we may need to adjust the configuration.

In webpack, the filename of the output can be adjusted by the chunkFileName child of the output. ChunkFileName is named in the same way as filename, but to ensure the uniqueness of the output filename, it is recommended to use one of the variables [name], [id], or [chunkhash]. Here [name] is usually the same as [id], only when webpackChunkName is set in import. The definition is as follows:

import(
  // The value of [name] can be adjusted by defining webpackChunkName
  /* webpackChunkName: "a-async" */
  // webpackMode can be selected with multiple values, please go to the help document for further information:
  // https://www.webpackjs.com/api/module-methods/#import-
  /* webpackMode: "lazy" */
  './a').then(({ default: a }) = > {
  console.log(a);
});
Copy the code

A -async. Js file name is output with the following chunkFileName configuration:

const path = require('path');

module.exports = {
  entry: './src/index.js'.output: {
    filename: 'bundle.js'.path: path.resolve(__dirname, 'dist'),
    chunkFilename: `[name].js`,}};Copy the code

If chunkFilename is not configured, the rule for loading file names on demand is exported based on filename. The specific rules are as follows:

  1. If the filename rule ensures that the generated filename is unique, the filename is generated according to the filename rule.
  2. If the filename rule cannot ensure that the generated filename is unique, the filename rule is preceded by [id]. To ensure that the only

For example, the configuration is as follows:

const path = require('path');

module.exports = {
  entry: './src/index.js'.output: {
    filename: 'bundle.js'.path: path.resolve(__dirname, 'dist'),}};Copy the code

The actual generated on-demand file is called [id].bundle.js.

How to implement load on demand

Webpack scans the source files for import/export, repackages and assembles new files based on the recognition results, and the generated file code is reassembled in a way that the browser can recognize. So to figure out how import() is loaded on demand, just read the code generated after weipack processing. Now let’s start with loading the ‘./a’ file on demand:

  / *! * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/index.js ***! \ * * * * * * * * * * * * * * * * * * * * * * /
  / *! dynamic exports provided */
  / *! all exports used */
  / * * * / (function(module.exports, __webpack_require__) {

  __webpack_require__.e/* import() */(0/ *! a-async */).then(__webpack_require__.bind(null./ *! ./a */ 82)).then(function (_ref) {
    var a = _ref.default;
    console.log(a);
  });
  
  / * * * / })
Copy the code

The code above means:

  1. Webpack_require.e corresponds to the import() method, which is the main method for asynchronous loading; The argument is 0 for chunk A-async
  2. After asynchronous loading, the webpack_require method is first executed, and the first entry is 82 for the ‘./a’ file. Then you execute your own code

Let’s take a look at the body of the __webpack_require__.e method:

/ * * * * * * / 	// This file contains only the entry chunk.
/ * * * * * * / 	// The chunk loading function for additional chunks
/ * * * * * * / 	__webpack_require__.e = function requireEnsure(chunkId) {
/ * * * * * * / 		var installedChunkData = installedChunks[chunkId];
  					// 0 Indicates that the file is successfully loaded and no further action is required
/ * * * * * * / 		if(installedChunkData === 0) {
/ * * * * * * / 			return new Promise(function(resolve) { resolve(); });
/ * * * * * * / 		}
/ * * * * * * /
/ * * * * * * / 		// a Promise means "currently loading".
  					// Loading
/ * * * * * * / 		if(installedChunkData) {
/ * * * * * * / 			return installedChunkData[2];
/ * * * * * * / 		}
/ * * * * * * /
/ * * * * * * / 		// setup Promise in chunk cache
  					// Store the content of the Promise object in installedChunks[chunkId] for later use
/ * * * * * * / 		var promise = new Promise(function(resolve, reject) {
/ * * * * * * / 			installedChunkData = installedChunks[chunkId] = [resolve, reject];
/ * * * * * * / 		});
/ * * * * * * / 		installedChunkData[2] = promise;
/ * * * * * * /
/ * * * * * * / 		// start chunk loading
  					// Generate a script tag for asynchronously loading js files
/ * * * * * * / 		var head = document.getElementsByTagName('head') [0];
/ * * * * * * / 		var script = document.createElement('script');
/ * * * * * * / 		script.type = "text/javascript";
/ * * * * * * / 		script.charset = 'utf-8';
/ * * * * * * / 		script.async = true;
/ * * * * * * / 		script.timeout = 120000;
/ * * * * * * /
/ * * * * * * / 		if (__webpack_require__.nc) {
/ * * * * * * / 			script.setAttribute("nonce", __webpack_require__.nc);
/ * * * * * * / 		}
  					// __webpack_require__.p is the address of __webpack_public_path__
/ * * * * * * / 		script.src = __webpack_require__.p + "" + ({"0":"a-async"}[chunkId]||chunkId) + ".async.js";
  					// Execute onScriptComplete after timeout
/ * * * * * * / 		var timeout = setTimeout(onScriptComplete, 120000);
/ * * * * * * / 		script.onerror = script.onload = onScriptComplete;
/ * * * * * * / 		function onScriptComplete() {
/ * * * * * * / 			// avoid mem leaks in IE.
/ * * * * * * / 			script.onerror = script.onload = null;
/ * * * * * * / 			clearTimeout(timeout);
/ * * * * * * / 			var chunk = installedChunks[chunkId];
  						// If the file loads successfully, chunk is set to 0; Only load failures are dealt with later
/ * * * * * * / 			if(chunk ! = =0) {
/ * * * * * * / 				if(chunk) {
/ * * * * * * / 					chunk[1] (new Error('Loading chunk ' + chunkId + ' failed.'));
/ * * * * * * / 				}
/ * * * * * * / 				installedChunks[chunkId] = undefined;
/ * * * * * * / 			}
/ * * * * * * / 		};
/ * * * * * * / 		head.appendChild(script);
/ * * * * * * /
/ * * * * * * / 		return promise;
/ * * * * * * / 	};
Copy the code

A dynamically created script tag is generated and loaded successfully, and the corresponding file content is executed first, and then the onload event is executed. So onScriptComplete mainly deals with load failures.

Script tag corresponding to the ‘./a’ file contents:

webpackJsonp([0] and {/ * * * / 34:
/ *! * * * * * * * * * * * * * * * * * *! * \! *** ./src/a.js ***! \ * * * * * * * * * * * * * * * * * * /
/ *! exports provided: default */
/ *! all exports used */
/ * * * / (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony export (immutable) */ __webpack_exports__["default"] = a;
console.log('Load file a');
function a() {
  console.log('I'm module A');
}

/ * * * /})});Copy the code

As soon as the ‘./a’ file comes in, the webpackJsonp method is executed and the contents of its own file are passed in as the second input value. The webpackJsonp method is defined as follows:

/ * * * * * * / 	window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
/ * * * * * * / 		// add "moreModules" to the modules object,
/ * * * * * * / 		// then flag all "chunkIds" as loaded and fire callback
/ * * * * * * / 		var moduleId, chunkId, i = 0, resolves = [], result;
/ * * * * * * / 		for(; i < chunkIds.length; i++) {/ * * * * * * / 			chunkId = chunkIds[i];
/ * * * * * * / 			if(installedChunks[chunkId]) {
/ * * * * * * / 				resolves.push(installedChunks[chunkId][0]);
/ * * * * * * / 			}
/ * * * * * * / 			installedChunks[chunkId] = 0;
/ * * * * * * / 		}
/ * * * * * * / 		for(moduleId in moreModules) {
/ * * * * * * / 			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/ * * * * * * / 				modules[moduleId] = moreModules[moduleId];
/ * * * * * * / 			}
/ * * * * * * / 		}
/ * * * * * * / 		if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
/ * * * * * * / 		while(resolves.length) {
/ * * * * * * / 			resolves.shift()();
/ * * * * * * / 		}
/ * * * * * * /
/ * * * * * * / 	};
Copy the code

The webpackJsonp method does three things in turn:

  1. InstalledChunks [chunkId] set to 0 to indicate successful loading (L5-11)
  2. Execute contents of ‘./a’ file (L12-16)
  3. The contents of the asynchronous appending file (L18-20)

The entire loading process is now complete.

Behavior of the Webpack exception

Above we looked at the simplest loading on demand case. What does WebPack do if a public module is referenced both by the main file and by an asynchronously loaded module? Look at the following example: module A, referenced by both modules B and C, B introduced synchronously by the main file, and C introduced asynchronously.

// The main file contents index.js
import b from './b';

b();

import(
  /* webpackChunkName: "c-async" */
  /* webpackMode: "lazy" */
  './c').then(({ default: c }) = > {
  console.log('Asynchronous load C', c);
});


// B module content b.js
import a from './a';

export default function b() {
  a();
  console.log('I'm module B');
}


// c module contents c.js
import a from './a';

export default function c() {
  a();
  console.log('I'm module C');
}


// a module content a.js
console.log('Load file a');

export default function a() {
  console.log('I'm module A');
}


Copy the code

Execution Result:We see that “load file A” is printed only once, that is, module A is loaded only once, as expected. Let’s look at the code for modules B and C generated by Webpack:

  / * * / 83
  / *! * * * * * * * * * * * * * * * * * *! * \! *** ./src/b.js ***! \ * * * * * * * * * * * * * * * * * * /
  / *! exports provided: default */
  / *! exports used: default */
  / * * * / (function(module, __webpack_exports__, __webpack_require__) {
  
  "use strict";
  /* harmony export (immutable) */ __webpack_exports__["a"] = b;
  /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__a__ = __webpack_require__(/ *! ./a */ 34);
  
  function b() {
    Object(__WEBPACK_IMPORTED_MODULE_0__a__["a" /* default */) ();console.log('I'm module B');
  }
  
  / * * * / })
  
  
/ * * * / 84:
/ *! * * * * * * * * * * * * * * * * * *! * \! *** ./src/c.js ***! \ * * * * * * * * * * * * * * * * * * /
/ *! exports provided: default */
/ *! all exports used */
/ * * * / (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony export (immutable) */ __webpack_exports__["default"] = c;
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__a__ = __webpack_require__(/ *! ./a */ 34);

function c() {
  Object(__WEBPACK_IMPORTED_MODULE_0__a__["a" /* default */) ();console.log('I'm module C');
}

/ * * * / })
Copy the code

Loading module A uses webpack_require, and then we find the body of the _webpack require method:

/ * * * * * * / 	// The require function
/ * * * * * * / 	function __webpack_require__(moduleId) {
/ * * * * * * /
/ * * * * * * / 		// Check if module is in cache
/ * * * * * * / 		if(installedModules[moduleId]) {
/ * * * * * * / 			return installedModules[moduleId].exports;
/ * * * * * * / 		}
/ * * * * * * / 		// Create a new module (and put it into the cache)
/ * * * * * * / 		var module = installedModules[moduleId] = {
/ * * * * * * / 			i: moduleId,
/ * * * * * * / 			l: false./ * * * * * * / 			exports: {},
/ * * * * * * / 			hot: hotCreateModule(moduleId),
/ * * * * * * / 			parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),
/ * * * * * * / 			children: []
/ * * * * * * / 		};
/ * * * * * * /
/ * * * * * * / 		// Execute the module function
/ * * * * * * / 		modules[moduleId].call(module.exports, module.module.exports, hotCreateRequire(moduleId));
/ * * * * * * /
/ * * * * * * / 		// Flag the module as loaded
/ * * * * * * / 		module.l = true;
/ * * * * * * /
/ * * * * * * / 		// Return the exports of the module
/ * * * * * * / 		return module.exports;
/ * * * * * * / 	}
Copy the code

The meaning of the above code is:

  1. Return if the module is already loaded (L5-7)
  2. If the module is not loaded, build the module object data and execute the module contents and output (L9-25); Note: modules here are loaded synchronously (already loaded), so there is no loading process.

So when module A is executed again, it returns directly, without going to step 2 of the _webpack require method (L9). _

Weibpack import() : weibpack import() : Weibpack import() : Weibpack import()