preface
The JS SDK builds Webpack VS Rollup and the code packaging volume of Webpack and Rollup is doubled. The main reason is that the Tree Shaking algorithm is different. Webpack Tree Shaking source code
Tree Shaking
Mechanism is briefly
Tree shaking was first proposed by the author of Rollup. Here’s a metaphor:
If you think of code packaging as making a cake. The traditional method is to throw in all the eggs (with the shell), stir them, put them in the oven, and then select and remove all the shells. Treeshaking, on the other hand, starts with a useful white egg yolk and whisks it into the cake.
Therefore, it is obviously more secure to find used code than unused code. Tree Shaking is finding used code, leaving unused code behind, and annotating it separately.
After separating the used and unused code, the unused code is removed through the compressor.
Use the premise
Since Tree Shaking uses ES6 Import and Export implementations to find used and unused code, Tree Shaking uses the following prerequisites: The source code must follow the ES6 module specification (import & export), if the CommonJS specification (require) cannot be used.
Webpack – Tree Shaking
The example analysis
Close the optimization
Webpack only turns Tree Shaking on in Production mode, so you need to set mode to Production. As you can see from the Tree Shaking mechanism in the previous section, we need to turn off Webpack’s code compressor to see how Webpack uses code, so we need to turn off Webpack optimization.
const path = require('path')
module.exports = {
entry: './src/index.js'.output: {
filename: 'bundle.js'.path: path.resolve(__dirname, 'dist')},mode: 'production'.optimization: {
minimize: false.concatenateModules: false
},
devtool: false
}
Copy the code
util.js
export function usedFunction() {
return 'usedFunction'
}
export function unusedFunction() {
return 'unusedFunction'
}
Copy the code
index.js
import {
usedFunction,
unusedFunction
} from './util'
let result1 = usedFunction()
// let result2 = unusedFunction()
console.log(result1)
Copy the code
Package the main part of the result bundle.js (sure enough, see Webpack’s code usage notes)
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ * * * * * * /
([
/* 0 */
/ * * * /
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (binding) */
__webpack_require__.d(__webpack_exports__, "a".function() {
return usedFunction;
});
/* unused harmony export unusedFunction */
function usedFunction() {
return 'usedFunction'
}
function unusedFunction() {
return 'unusedFunction'
}
/ * * * /
}),
/ * 1 * /
/ * * * /
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */
var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
let result1 = Object(_util__WEBPACK_IMPORTED_MODULE_0__[ /* usedFunction */ "a") ()// let result2 = unusedFunction()
console.log(result1)
/ * * * /
})
/ * * * * * * /
]);
Copy the code
Obviously: WebPack is responsible for tagging the code, marking import & Export as class 3
- The used export is marked /* Harmony export ([type]) */, where [type] is related to webpack and can be binding, immutable, etc.
- An unused export is marked as /* unused Harmony export [FuncName] */, where [FuncName] is the name of the export method
- All imports are marked as /* harmony import */
Open the optimization
const path = require('path')
module.exports = {
entry: './src/index.js'.output: {
filename: 'bundle.js'.path: path.resolve(__dirname, 'dist')},mode: 'production'.optimization: {
minimize: true.concatenateModules: true
},
devtool: false
}
Copy the code
Packing results:
! function(e) {
var t = {};
function n(r) {
if (t[r]) return t[r].exports;
var o = t[r] = {
i: r,
l:!1.exports: {}};return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports
}
n.m = e, n.c = t, n.d = function(e, t, r) {
n.o(e, t) || Object.defineProperty(e, t, {
enumerable:!0.get: r
})
}, n.r = function(e) {
"undefined"! =typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
value: "Module"
}), Object.defineProperty(e, "__esModule", {
value:!0
})
}, n.t = function(e, t) {
if (1 & t && (e = n(e)), 8 & t) return e;
if (4 & t && "object"= =typeof e && e && e.__esModule) return e;
var r = Object.create(null);
if (n.r(r), Object.defineProperty(r, "default", {
enumerable:!0.value: e
}), 2 & t && "string"! =typeof e)
for (var o in e) n.d(r, o, function(t) {
return e[t]
}.bind(null, o));
return r
}, n.n = function(e) {
var t = e && e.__esModule ? function() {
return e.default
} : function() {
return e
};
return n.d(t, "a", t), t
}, n.o = function(e, t) {
return Object.prototype.hasOwnProperty.call(e, t)
}, n.p = "", n(n.s = 0)
}([function(e, t, n) {
"use strict";
n.r(t);
console.log("usedFunction")}]);Copy the code
Obviously, code will be streamlined based on code annotations, and everything that doesn’t work will be removed.
Case Analysis summary
Webpack Tree Shaking is divided into two steps:
- The first step is to annotate code usage
- The second step is to delete all unused code.
Webpack Tree Shaking source code analysis
Static analysis of Webpack code, annotating code usage
By searching Webpack source code, the section containing Harmony export can find the specific implementation of used export and unused export annotation:
lib/dependencies/HarmonyExportInitFragment.js
class HarmonyExportInitFragment extends InitFragment {
/ * * *@param {string} exportsArgument the exports identifier
* @param {Map<string, string>} exportMap mapping from used name to exposed variable name
* @param {Set<string>} unusedExports list of unused export names
*/
constructor(exportsArgument, exportMap = EMPTY_MAP, unusedExports = EMPTY_SET) {
super(undefined, InitFragment.STAGE_HARMONY_EXPORTS, 1."harmony-exports");
this.exportsArgument = exportsArgument;
this.exportMap = exportMap;
this.unusedExports = unusedExports;
}
merge(other) {
let exportMap;
if (this.exportMap.size === 0) {
exportMap = other.exportMap;
} else if (other.exportMap.size === 0) {
exportMap = this.exportMap;
} else {
exportMap = new Map(other.exportMap);
for (const [key, value] of this.exportMap) {
if (!exportMap.has(key)) exportMap.set(key, value);
}
}
let unusedExports;
if (this.unusedExports.size === 0) {
unusedExports = other.unusedExports;
} else if (other.unusedExports.size === 0) {
unusedExports = this.unusedExports;
} else {
unusedExports = new Set(other.unusedExports);
for (const value of this.unusedExports) { unusedExports.add(value); }}return new HarmonyExportInitFragment(
this.exportsArgument,
exportMap,
unusedExports
);
}
/ * * *@param {GenerateContext} generateContext context for generate
* @returns {string|Source} the source code that will be included as initialization code
*/
getContent({ runtimeTemplate, runtimeRequirements }) {
runtimeRequirements.add(RuntimeGlobals.exports);
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
const unusedPart =
this.unusedExports.size > 1 ?
`/* unused harmony exports ${joinIterableWithComma(
this.unusedExports
)} */\n` :
this.unusedExports.size > 0 ?
`/* unused harmony export The ${this.unusedExports.values().next().value
} */\n` :
"";
const definitions = [];
for (const [key, value] of this.exportMap) {
definitions.push(
`\n/* harmony export */ The ${JSON.stringify(
key
)}: ${runtimeTemplate.returningFunction(value)}`
);
}
const definePart =
this.exportMap.size > 0 ?
`/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(The ${this.exportsArgument
}, {${definitions.join(",")}\n/* harmony export */ }); \n` :
"";
return `${definePart}${unusedPart}`; }}Copy the code
harmony export
GetContent handles exportMap and replaces the original export
const definePart =
this.exportMap.size > 0 ?
`/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(The ${this.exportsArgument
}, {${definitions.join(",")}\n/* harmony export */ }); \n` :
"";
return `${definePart}${unusedPart}` ;
}
Copy the code
unused harmony exports
GetContent handles unusedExports and replaces the original export
const unusedPart =
this.unusedExports.size > 1 ?
`/* unused harmony exports ${joinIterableWithComma(
this.unusedExports
)} */\n` :
this.unusedExports.size > 0 ?
`/* unused harmony export The ${this.unusedExports.values().next().value
} */\n` :
"";
Copy the code
lib/dependencies/HarmonyExportSpecifierDependency.js
Statement informs and unused, and calls to replace HarmonyExportInitFragment off the export in the source code
HarmonyExportSpecifierDependency.Template = class HarmonyExportSpecifierDependencyTemplate extends NullDependency.Template {
/ * * *@param {Dependency} dependency the dependency for which the template should be applied
* @param {ReplaceSource} source the current replace source which can be modified
* @param {DependencyTemplateContext} templateContext the context object
* @returns {void}* /
apply(
dependency,
source,
{ module, moduleGraph, initFragments, runtimeRequirements, runtime }
) {
const dep = / * *@type {HarmonyExportSpecifierDependency} * / (dependency);
const used = moduleGraph
.getExportsInfo(module)
.getUsedName(dep.name, runtime);
if(! used) {const set = new Set(a); set.add(dep.name ||"namespace");
initFragments.push(
new HarmonyExportInitFragment(module.exportsArgument, undefined, set)
);
return;
}
const map = new Map(a); map.set(used,`/* binding */ ${dep.id}`);
initFragments.push(
new HarmonyExportInitFragment(module.exportsArgument, map, undefined)); }};Copy the code
lib/dependencies/HarmonyExportSpecifierDependency.js
Pass moduleGraph to get all export name values
/**
* Returns the exported names
* @param {ModuleGraph} moduleGraph module graph
* @returns {ExportsSpec | undefined} export names
*/
getExports(moduleGraph) {
return {
exports: [this.name],
terminalBinding: true.dependencies: undefined
};
}
Copy the code
ModuleGraph (Build graph structure of ES6 module specification)
Lib/modulegraph.js (too much code, not to show)
class ModuleGraph {
constructor() {
/ * *@type {Map<Dependency, ModuleGraphDependency>} * /
this._dependencyMap = new Map(a);/ * *@type {Map<Module, ModuleGraphModule>} * /
this._moduleMap = new Map(a);/ * *@type {Map<Module, Set<ModuleGraphConnection>>} * /
this._originMap = new Map(a);/ * *@type {Map<any, Object>} * /
this._metaMap = new Map(a);// Caching
this._cacheModuleGraphModuleKey1 = undefined;
this._cacheModuleGraphModuleValue1 = undefined;
this._cacheModuleGraphModuleKey2 = undefined;
this._cacheModuleGraphModuleValue2 = undefined;
this._cacheModuleGraphDependencyKey = undefined;
this._cacheModuleGraphDependencyValue = undefined; }...Copy the code
Call function in the corresponding ModuleGraph for code static analysis at different stages of processing, build the ModuleGraph to prepare for export and import annotations, etc.
Take Compilation as an example.
Compilation
Lib/compiler.js (part of the code) pushes the analyzed modules into ModuleGraph at compile time.
/ * * *@param {Chunk} chunk target chunk
* @param {RuntimeModule} module runtime module
* @returns {void}* /
addRuntimeModule(chunk, module) {
// Deprecated ModuleGraph association
ModuleGraph.setModuleGraphForModule(module.this.moduleGraph);
// add it to the list
this.modules.add(module);
this._modules.set(module.identifier(), module);
// connect to the chunk graph
this.chunkGraph.connectChunkAndModule(chunk, module);
this.chunkGraph.connectChunkAndRuntimeModule(chunk, module);
// attach runtime module
module.attach(this, chunk);
// Setup internals
const exportsInfo = this.moduleGraph.getExportsInfo(module);
exportsInfo.setHasProvideInfo();
if (typeof chunk.runtime === "string") {
exportsInfo.setUsedForSideEffectsOnly(chunk.runtime);
} else if (chunk.runtime === undefined) {
exportsInfo.setUsedForSideEffectsOnly(undefined);
} else {
for (const runtime ofchunk.runtime) { exportsInfo.setUsedForSideEffectsOnly(runtime); }}this.chunkGraph.addModuleRuntimeRequirements(
module,
chunk.runtime,
new Set([RuntimeGlobals.requireScope])
);
// runtime modules don't need ids
this.chunkGraph.setModuleId(module."");
// Call hook
this.hooks.runtimeModule.call(module, chunk);
}
Copy the code
Source code analysis summary
- Webpack puts the discovered modules into the ModuleGraph at compile time
- HarmonyExportSpecifierDependency HarmonyImportSpecifierDependency and identify the import and export of the module.
- HarmonyExportSpecifierDependency recognition, informs the export and unused export
- 2 and unused
4.1 Replace export of used export with /* harmony export ([type])/import 4.2 Replace export of unused export with/unused harmony export [FuncName] */
Based on the above source code analysis, it can be marked as export and import units
Export class example for Tree Shaking
util.js
export default class Util {
usedFunction () {
return 'usedFunction'
}
unusedFunction () {
return 'unusedFunction'}}Copy the code
index.js
import Util from './util'
let util = new Util()
let result1 = util.usedFunction()
// let result2 = unusedFunction()
console.log(result1)
Copy the code
Package the main parts of the result bundle.js
"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a".function() { return Util; });
class Util {
usedFunction () {
return 'usedFunction'
}
unusedFunction () {
return 'unusedFunction'}}/ * * * / }),
/ * 1 * /
/ * * * / (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
let util = new _util__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"] ()let result1 = util.usedFunction()
// let result2 = unusedFunction()
console.log(result1)
/ * * * / })
/ * * * * * * / ]);
Copy the code
Open optimization’s packaging results
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var u=n[r]={i:r,l:!1.exports: {}};return e[r].call(u.exports,u,u.exports,t),u.l=!0,u.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0.get:r})},t.r=function(e){"undefined"! =typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule", {value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"= =typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default", {enumerable:!0.value:e}),2&n&&"string"! =typeof e)for(var u in e)t.d(r,u,function(n){return e[n]}.bind(null,u));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)} ([function(e,n,t){"use strict"; t.r(n);let r=(new class{usedFunction(){return"usedFunction"}unusedFunction(){return"unusedFunction"}}).usedFunction();console.log(r)}]);
Copy the code
Function, the unused part of the class, is not marked unused export in the package result. The available Webpack is marked for the class as a whole (marked as being used), not individually for internal methods.
Combined with examples and source code analysis summary:
- Write code using ES6 module syntax so Tree Shaking works.
- The utility class functions should be output as separate functions, not as a single object or class, and avoid packaging objects and unused parts of the class.
The author
Benny Shi |