tree-shaking

来源:https://www.cnblogs.com/dtux/archive/2022/06/15/16377750.html
-Advertisement-
Play Games

來源 tree-shaking 最早由 Rich Harris 在 rollup 中提出。 為了減少最終構建體積而誕生。 以下是 MDN 中的說明: tree-shaking 是一個通常用於描述移除 JavaScript 上下文中的未引用代碼(dead-code) 行為的術語。 它依賴於 ES201 ...


來源

tree-shaking 最早由 Rich Harrisrollup 中提出。

為了減少最終構建體積而誕生。

以下是 MDN 中的說明:

tree-shaking 是一個通常用於描述移除 JavaScript 上下文中的未引用代碼(dead-code) 行為的術語。

它依賴於 ES2015 中的 import 和 export 語句,用來檢測代碼模塊是否被導出、導入,且被 JavaScript 文件使用。

在現代 JavaScript 應用程式中,我們使用模塊打包(如 webpack 或 Rollup)將多個 JavaScript 文件打包為單個文件時自動刪除未引用的代碼。這對於準備預備發佈代碼的工作非常重要,這樣可以使最終文件具有簡潔的結構和最小化大小。

tree-shaking VS dead code elimination

說起 tree-shaking 不得不說起 dead code elimination,簡稱 DCE

很多人往往把 tree-shaking 當作是一種實現 DCE 的技術。如果都是同一種東西,最終的目標是一致的(更少的代碼)。為什麼要重新起一個名字叫做 tree-shaking 呢?

tree-shaking 術語的發明者 Rich Harris 在他寫的一篇《tree-shaking versus dead code elimination》告訴了我們答案。

Rich Harris 引用了一個做蛋糕的例子。原文如下:

Bad analogy time: imagine that you made cakes by throwing whole eggs into the mixing bowl and smashing them up, instead of cracking them open and pouring the contents out. Once the cake comes out of the oven, you remove the fragments of eggshell, except that’s quite tricky so most of the eggshell gets left in there.

You’d probably eat less cake, for one thing.

That’s what dead code elimination consists of — taking the finished product, and imperfectly removing bits you don’t want. tree-shaking, on the other hand, asks the opposite question: given that I want to make a cake, which bits of what ingredients do I need to include in the mixing bowl?

Rather than excluding dead code, we’re including live code. Ideally the end result would be the same, but because of the limitations of static analysis in JavaScript that’s not the case. Live code inclusion gets better results, and is prima facie a more logical approach to the problem of preventing our users from downloading unused code.

簡單來說:DCE 好比做蛋糕時,直接放入整個雞蛋,做完時再從蛋糕中取出蛋殼。而 tree-shaking 則是先取出蛋殼,在進行做蛋糕。兩者結果相同,但是過程是完全不同的。

dead code

dead code 一般具有以下幾個特征:

  • 代碼不會被執行,不可到達
  • 代碼執行的結果不會被用到
  • 代碼只會影響死變數(只寫不讀)

使用 webpackmode: development 模式下對以下代碼進行打包:

function app() {
    var test = '我是app';
    function set() {
        return 1;
    }
    return test;
    test = '無法執行';
    return test;
}

export default app;

最終打包結果:

eval(
    "function app() {\n    var test = '我是app';\n    function set() {\n        return 1;\n    }\n    return test;\n    test = '無法執行';\n    return test;\n}\n\napp();\n\n\n//# sourceURL=webpack://webpack/./src/main.js?"
);

可以看到打包的結果內,還是存在無法執行到的代碼塊。

webpack 不支持 dead code elimination 嗎?是的,webpack 不支持。

原來,在 webpack 中實現 dead code elimination 功能並不是 webpack 本身, 而是大名鼎鼎的 uglify

通過閱讀源碼發現,在 mode: development 模式下,不會載入 terser-webpack-plugin 插件。

// lib/config/defaults.js
D(optimization, 'minimize', production);
A(optimization, 'minimizer', () => [
    {
        apply: (compiler) => {
            // Lazy load the Terser plugin
            const TerserPlugin = require('terser-webpack-plugin');
            new TerserPlugin({
                terserOptions: {
                    compress: {
                        passes: 2
                    }
                }
            }).apply(compiler);
        }
    }
]);

// lib/WebpackOptionsApply.js
if (options.optimization.minimize) {
    for (const minimizer of options.optimization.minimizer) {
        if (typeof minimizer === 'function') {
            minimizer.call(compiler, compiler);
        } else if (minimizer !== '...') {
            minimizer.apply(compiler);
        }
    }
}

terser-webpack-plugin 插件內部使用了 uglify 實現的。

我們在 mode: production 模式下進行打包。

// 格式化後結果
(() => {
    var r = {
            225: (r) => {
                r.exports = '我是app';
            }
        },
    // ...
})();

可以看到最終的結果,已經刪除了不可執行部分的代碼。除此之外,還幫我們壓縮了代碼,刪除了註釋等功能。

tree shaking 無效

tree shaking 本質上是通過分析靜態的 ES 模塊,來剔除未使用代碼的。

_ESModule__ 的特點_

只能作為模塊頂層的語句出現,不能出現在 function 裡面或是 if 裡面。(ECMA-262 15.2)
import 的模塊名只能是字元串常量。(ECMA-262 15.2.2)
不管 import 的語句出現的位置在哪裡,在模塊初始化的時候所有的 import 都必須已經導入完成。(ECMA-262 15.2.1.16.4 - 8.a)
import binding 是 immutable 的,類似 const。比如說你不能 import { a } from ‘./a’ 然後給 a 賦值個其他什麼東西。(ECMA-262 15.2.1.16.4 - 12.c.3)
—–引用自尤雨溪

我們來看看 tree shaking 的功效。

我們有一個模塊

// ./src/app.js
export const firstName = 'firstName'

export function getName ( x ) {
    return x.a
}

getName({ a: 123 })

export function app ( x ) {
    return x * x * x;
}

export default app;

底下是 7 個實例。

// 1*********************************************
// import App from './app'

// export function main() {
//     var test = '我是index';
//     return test;
// }

// console.log(main)

// 2*********************************************

// import App from './app'

// export function main() {
//     var test = '我是index';
//     console.log(App(1))
//     return test;
// }

// console.log(main)


// 3*********************************************

// import App from './app'

// export function main() {
//     var test = '我是index';
//     App.square(1)
//     return test;
// }

// console.log(main)


// 4*********************************************

// import App from './app'

// export function main() {
//     var test = '我是index';
//     let methodName = 'square'
//     App[methodName](1)
//     return test;
// }

// console.log(main)

// 6*********************************************

// import * as App from './app'

// export function main() {
//     var test = '我是index';
//     App.square(1)
//     return test;
// }

// console.log(main)

// 7*********************************************

// import * as App from './app'

// export function main() {
//     var test = '我是index';
//     let methodName = 'square'
//     App[methodName](1)
//     return test;
// }

// console.log(main)

使用 最簡單的webpack配置進行打包

// webpack.config.js
module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'dist.js'
    },
    mode: 'production'
};

通過結果可以看到,前 6 中的打包結果,都對死代碼進行了消除,只有第 7 種,消除失敗。

/* ... */
const r = 'firstName';
function o(e) {
	return e.a;
}
function n(e) {
	return e * e * e;
}
o({ a: 123 });
const a = n;
console.log(function () {
	return t.square(1), '我是index';
});

本人沒有詳細瞭解過,只能猜測下,由於 JavaScript 動態語言的特性使得靜態分析比較困難,目前的的解析器是通過靜態解析的,還無法分析全量導入,動態使用的語法。

對於更多 tree shaking 執行相關的可以參考一下鏈接:

當然了,機智的程式員是不會被這個給難住的,既然靜態分析不行,那就由開發者手動來將文件標記為無副作用(side-effect-free)。

tree shaking 和 sideEffects

sideEffects 支持兩種寫法,一種是 false,另一種是數組

  • 如果所有代碼都不包含副作用,我們就可以簡單地將該屬性標記為 false
  • 如果你的代碼確實有一些副作用,可以改為提供一個數組

可以在 package.js 中進行設置。

// boolean
{
  "sideEffects": false
}

// array
{
  "sideEffects": ["./src/app.js", "*.css"]
}

也可以在 module.rules 中進行設置。

module.exports = {
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /(node_modules)/,
        use: {
          loader: 'babel-loader',
        },
        sideEffects: false || []
      }
    ]
  },
}

設置了 sideEffects: false,後在重新打包

 var e = {
            225: (e, r, t) => {
                (e = t.hmd(e)).exports = '我是main';
            }
        },

只剩下 main.js 模塊的代碼,已經把 app.js 的代碼消除了。

usedExports

webpack 中除了 sideEffects 還提供了一種另一種標記消除的方式。那就是通過配置項 usedExports

由 optimization.usedExports 收集的信息會被其它優化手段或者代碼生成使用,比如未使用的導出內容不會被生成,當所有的使用都適配,導出名稱會被處理做單個標記字元。 在壓縮工具中的無用代碼清除會受益於該選項,而且能夠去除未使用的導出內容。

mode: productions 下是預設開啟的。

module.exports = {
  //...
  optimization: {
    usedExports: true,
  },
};

usedExports 會使用 terser 判斷代碼有沒有 sideEffect,如果沒有用到,又沒有 sideEffect 的話,就會在打包時替它標記上 unused harmony。

最後由 TerserUglifyJSDCE 工具“搖”掉這部分無效代碼。

terser 測試

tree shaking 實現原理

tree shaking 本身也是採用靜態分析的方法。

程式靜態分析(Static Code Analysis)是指在不運行代碼的方式下,通過詞法分析、語法分析、控制流分析、數據流分析等技術對程式代碼進行掃描,驗證代碼是否滿足規範性、安全性、可靠性、可維護性等指標的一種代碼分析技術

tree shaking 使用的前提是模塊必須採用ES6Module語法,因為tree Shaking 依賴 ES6 的語法:importexport

接下來我們來看看遠古版本的 rollup 是怎麼實現 tree shaking 的。

  1. 根據入口模塊內容初始化 Module,並使用 acorn 進行 ast 轉化
  2. 分析 ast。 尋找 importexport 關鍵字,建立依賴關係
  3. 分析 ast,收集當前模塊存在的函數、變數等信息
  4. 再一次分析 ast, 收集各函數變數的使用情況,因為我們是根據依賴關係進行收集代碼,如果函數變數未被使用,
  5. 根據收集到的函數變數標識符等信息,進行判斷,如果是 import,則進行 Module 的創建,重新走上幾步。否則的話,把對應的代碼信息存放到一個統一的 result 中。
  6. 根據最終的結果生成 bundle

file

源碼版本:v0.3.1

通過 entry 入口文件進行創建 bundle,執行 build 方法,開始進行打包。

export function rollup ( entry, options = {} ) {
	const bundle = new Bundle({
		entry,
		resolvePath: options.resolvePath
	});

	return bundle.build().then( () => {
		return {
			generate: options => bundle.generate( options ),
			write: ( dest, options = {} ) => {
				let { code, map } = bundle.generate({
					dest,
					format: options.format,
					globalName: options.globalName
				});

				code += `\n//# ${SOURCEMAPPING_URL}=${basename( dest )}.map`;

				return Promise.all([
					writeFile( dest, code ),
					writeFile( dest + '.map', map.toString() )
				]);
			}
		};
	});
}

build 內部執行 fetchModule 方法,根據文件名,readFile 讀取文件內容,創建 Module

build () {
    return this.fetchModule( this.entryPath, null )
        .then( entryModule => {
            this.entryModule = entryModule;

            if ( entryModule.exports.default ) {
                let defaultExportName = makeLegalIdentifier( basename( this.entryPath ).slice( 0, -extname( this.entryPath ).length ) );
                while ( entryModule.ast._scope.contains( defaultExportName ) ) {
                    defaultExportName = `_${defaultExportName}`;
                }

                entryModule.suggestName( 'default', defaultExportName );
            }

            return entryModule.expandAllStatements( true );
        })
        .then( statements => {
            this.statements = statements;
            this.deconflict();
        });
}

fetchModule ( importee, importer ) {
    return Promise.resolve( importer === null ? importee : this.resolvePath( importee, importer ) )
        .then( path => {
                /*
                    緩存處理
                */

                this.modulePromises[ path ] = readFile( path, { encoding: 'utf-8' })
                    .then( code => {
                        const module = new Module({
                            path,
                            code,
                            bundle: this
                        });

                        return module;
                    });

            return this.modulePromises[ path ];
        });
}

根據讀取到的文件內容,使用 acorn 編譯器進行進行 ast 的轉化。

// 
export default class Module {
    constructor ({ path, code, bundle }) {
		/*
        初始化
        */
		this.ast = parse(code, {
			ecmaVersion: 6,
			sourceType: 'module',
			onComment: (block, text, start, end) =>
			this.comments.push({ block, text, start, end })
		});
		this.analyse();
	}

file

遍歷節點信息。尋找 importexport 關鍵字,這一步就是我們常說的根據 esm 的靜態結構進行分析。

import 的信息,收集到 this.imports 對象中,把 exports 的信息,收集到 this.exports 中.

this.ast.body.forEach( node => {
	let source;
	if ( node.type === 'ImportDeclaration' ) {
		source = node.source.value;

		node.specifiers.forEach( specifier => {
			const isDefault = specifier.type === 'ImportDefaultSpecifier';
			const isNamespace = specifier.type === 'ImportNamespaceSpecifier';

			const localName = specifier.local.name;
			const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;

			if ( has( this.imports, localName ) ) {
				const err = new Error( `Duplicated import '${localName}'` );
				err.file = this.path;
				err.loc = getLocation( this.code.original, specifier.start );
				throw err;
			}

			this.imports[ localName ] = {
				source, // 模塊id
				name,
				localName
			};
		});
	}

	else if ( /^Export/.test( node.type ) ) {
		if ( node.type === 'ExportDefaultDeclaration' ) {
			const isDeclaration = /Declaration$/.test( node.declaration.type );

			this.exports.default = {
				node,
				name: 'default',
				localName: isDeclaration ? node.declaration.id.name : 'default',
				isDeclaration
			};
		}

		else if ( node.type === 'ExportNamedDeclaration' ) {
			// export { foo } from './foo';
			source = node.source && node.source.value;

			if ( node.specifiers.length ) {
				node.specifiers.forEach( specifier => {
					const localName = specifier.local.name;
					const exportedName = specifier.exported.name;

					this.exports[ exportedName ] = {
						localName,
						exportedName
					};

					if ( source ) {
						this.imports[ localName ] = {
							source,
							localName,
							name: exportedName
						};
					}
				});
			}

			else {
				let declaration = node.declaration;

				let name;

				if ( declaration.type === 'VariableDeclaration' ) {
					name = declaration.declarations[0].id.name;
				} else {
					name = declaration.id.name;
				}

				this.exports[ name ] = {
					node,
					localName: name,
					expression: declaration
				};
			}
		}
	}
}

file

	analyse () {
		// imports and exports, indexed by ID
		this.imports = {};
		this.exports = {};

        // 遍歷 ast 查找對應的 import、export 關聯
		this.ast.body.forEach( node => {
			let source;

			// import foo from './foo';
			// import { bar } from './bar';
			if ( node.type === 'ImportDeclaration' ) {
				source = node.source.value;

				node.specifiers.forEach( specifier => {
					const isDefault = specifier.type === 'ImportDefaultSpecifier';
					const isNamespace = specifier.type === 'ImportNamespaceSpecifier';

					const localName = specifier.local.name;
					const name = isDefault ? 'default' : isNamespace ? '*' : specifier.imported.name;

					if ( has( this.imports, localName ) ) {
						const err = new Error( `Duplicated import '${localName}'` );
						err.file = this.path;
						err.loc = getLocation( this.code.original, specifier.start );
						throw err;
					}

					this.imports[ localName ] = {
						source, // 模塊id
						name,
						localName
					};
				});
			}

			else if ( /^Export/.test( node.type ) ) {
				// export default function foo () {}
				// export default foo;
				// export default 42;
				if ( node.type === 'ExportDefaultDeclaration' ) {
					const isDeclaration = /Declaration$/.test( node.declaration.type );

					this.exports.default = {
						node,
						name: 'default',
						localName: isDeclaration ? node.declaration.id.name : 'default',
						isDeclaration
					};
				}

				// export { foo, bar, baz }
				// export var foo = 42;
				// export function foo () {}
				else if ( node.type === 'ExportNamedDeclaration' ) {
					// export { foo } from './foo';
					source = node.source && node.source.value;

					if ( node.specifiers.length ) {
						// export { foo, bar, baz }
						node.specifiers.forEach( specifier => {
							const localName = specifier.local.name;
							const exportedName = specifier.exported.name;

							this.exports[ exportedName ] = {
								localName,
								exportedName
							};

							if ( source ) {
								this.imports[ localName ] = {
									source,
									localName,
									name: exportedName
								};
							}
						});
					}

					else {
						let declaration = node.declaration;

						let name;

						if ( declaration.type === 'VariableDeclaration' ) {
							name = declaration.declarations[0].id.name;
						} else {
							name = declaration.id.name;
						}

						this.exports[ name ] = {
							node,
							localName: name,
							expression: declaration
						};
					}
				}
			}
		}

        // 查找函數,變數,類,塊級作用與等,並根據引用關係進行關聯
        analyse( this.ast, this.code, this );     
}

接下來查找函數,變數,類,塊級作用與等,並根據引用關係進行關聯。

使用 magicString 為每一個 statement 節點增加內容修改的功能。

遍歷整顆 ast 樹,先初始化一個 Scope,作為當前模塊的命名空間。如果是函數或塊級作用域等則新建一個 Scope。各 Scope 之間通過 parent 進行關聯,建立起一個根據命名空間關係樹。

如果是變數和函數,則與當前的 Scope 進行關聯, 把對應的標識符名稱增加到 Scope 的中。到這一步,已經收集到了各節點上出現的函數和變數。

file

接下來,再一次遍歷 ast。查找變數函數,是否只是被讀取過,或者只是修改過。

根據 Identifier 類型查找標識符,如果當前標識符能在 Scope 中找到,說明有對其進行過讀取。存放在 _dependsOn 集合中。

file

接下來根據 AssignmentExpressionUpdateExpressionCallExpression 類型節點,收集我們的標識符,有沒有被修改過或被當前參數傳遞過。並將結果存放在 _modifies 中。

function analyse(ast, magicString, module) {
	var scope = new Scope();
	var currentTopLevelStatement = undefined;

	function addToScope(declarator) {
		var name = declarator.id.name;
		scope.add(name, false);

		if (!scope.parent) {
			currentTopLevelStatement._defines[name] = true;
		}
	}

	function addToBlockScope(declarator) {
		var name = declarator.id.name;
		scope.add(name, true);

		if (!scope.parent) {
			currentTopLevelStatement._defines[name] = true;
		}
	}

	// first we need to generate comprehensive scope info
	var previousStatement = null;
	var commentIndex = 0;

	ast.body.forEach(function (statement) {
		currentTopLevelStatement = statement; // so we can attach scoping info

		Object.defineProperties(statement, {
			_defines: { value: {} },
			_modifies: { value: {} },
			_dependsOn: { value: {} },
			_included: { value: false, writable: true },
			_module: { value: module },
			_source: { value: magicString.snip(statement.start, statement.end) }, // TODO don't use snip, it's a waste of memory
			_margin: { value: [0, 0] },
			_leadingComments: { value: [] },
			_trailingComment: { value: null, writable: true } });

		var trailing = !!previousStatement;

		// attach leading comment
		do {
			var comment = module.comments[commentIndex];

			if (!comment || comment.end > statement.start) break;

			// attach any trailing comment to the previous statement
			if (trailing && !/\n/.test(magicString.slice(previousStatement.end, comment.start))) {
				previousStatement._trailingComment = comment;
			}

			// then attach leading comments to this statement
			else {
				statement._leadingComments.push(comment);
			}

			commentIndex += 1;
			trailing = false;
		} while (module.comments[commentIndex]);

		// determine margin
		var previousEnd = previousStatement ? (previousStatement._trailingComment || previousStatement).end : 0;
		var start = (statement._leadingComments[0] || statement).start;

		var gap = magicString.original.slice(previousEnd, start);
		var margin = gap.split('\n').length;

		if (previousStatement) previousStatement._margin[1] = margin;
		statement._margin[0] = margin;

		walk(statement, {
			enter: function (node) {
				var newScope = undefined;

				magicString.addSourcemapLocation(node.start);

				switch (node.type) {
					case 'FunctionExpression':
					case 'FunctionDeclaration':
					case 'ArrowFunctionExpression':
						var names = node.params.map(getName);

						if (node.type === 'FunctionDeclaration') {
							addToScope(node);
						} else if (node.type === 'FunctionExpression' && node.id) {
							names.push(node.id.name);
						}

						newScope = new Scope({
							parent: scope,
							params: names, // TODO rest params?
							block: false
						});

						break;

					case 'BlockStatement':
						newScope = new Scope({
							parent: scope,
							block: true
						});

						break;

					case 'CatchClause':
						newScope = new Scope({
							parent: scope,
							params: [node.param.name],
							block: true
						});

						break;

					case 'VariableDeclaration':
						node.declarations.forEach(node.kind === 'let' ? addToBlockScope : addToScope); // TODO const?
						break;

					case 'ClassDeclaration':
						addToScope(node);
						break;
				}

				if (newScope) {
					Object.defineProperty(node, '_scope', { value: newScope });
					scope = newScope;
				}
			},
			leave: function (node) {
				if (node === currentTopLevelStatement) {
					currentTopLevelStatement = null;
				}

				if (node._scope) {
					scope = scope.parent;
				}
			}
		});

		previousStatement = statement;
	});

	// then, we need to find which top-level dependencies this statement has,
	// and which it potentially modifies
	ast.body.forEach(function (statement) {
		function checkForReads(node, parent) {
			if (node.type === 'Identifier') {
				// disregard the `bar` in `foo.bar` - these appear as Identifier nodes
				if (parent.type === 'MemberExpression' && node !== parent.object) {
					return;
				}

				// disregard the `bar` in { bar: foo }
				if (parent.type === 'Property' && node !== parent.value) {
					return;
				}

				var definingScope = scope.findDefiningScope(node.name);

				if ((!definingScope || definingScope.depth === 0) && !statement._defines[node.name]) {
					statement._dependsOn[node.name] = true;
				}
			}
		}

		function checkForWrites(node) {
			function addNode(node, disallowImportReassignments) {
				while (node.type === 'MemberExpression') {
					node = node.object;
				}

				// disallow assignments/updates to imported bindings and namespaces
				if (disallowImportReassignments && has(module.imports, node.name) && !scope.contains(node.name)) {
					var err = new Error('Illegal reassignment to import \'' + node.name + '\'');
					err.file = module.path;
					err.loc = getLocation(module.code.toString(), node.start);
					throw err;
				}

				if (node.type !== 'Identifier') {
					return;
				}

				statement._modifies[node.name] = true;
			}

			if (node.type === 'AssignmentExpression') {
				addNode(node.left, true);
			} else if (node.type === 'UpdateExpression') {
				addNode(node.argument, true);
			} else if (node.type === 'CallExpression') {
				node.arguments.forEach(function (arg) {
					return addNode(arg, false);
				});
			}

			// TODO UpdateExpressions, method calls?
		}

		walk(statement, {
			enter: function (node, parent) {
				// skip imports
				if (/^Import/.test(node.type)) return this.skip();

				if (node._scope) scope = node._scope;

				checkForReads(node, parent);
				checkForWrites(node, parent);

				//if ( node.type === 'ReturnStatement')
			},
			leave: function (node) {
				if (node._scope) scope = scope.parent;
			}
		});
	});

	ast._scope = scope;
}

執行完結果如下:

file

在上一步種,我們為函數,變數,類,塊級作用與等聲明與我們當前節點進行了關聯,現在要把節點上的這些信息,統一收集起來,放到 Module

//  
this.ast.body.forEach( statement => {
	Object.keys( statement._defines ).forEach( name => {
		this.definitions[ name ] = statement;
	});

	Object.keys( statement._modifies ).forEach( name => {
		if ( !has( this.modifications, name ) ) {
			this.modifications[ name ] = [];
		}

		this.modifications[ name ].push( statement );
	});
});

file

從中我們可以看到每個 statement 中,依賴了哪些,修改了哪些。

當我們在入口模塊的操作完成後,在遍歷 statement 節點,根據 _dependsOn 的中的信息,執行 define

如果 _dependsOn 的數據,在 this.imports 中,能夠找到,說明該標識符是一個導入模塊,調用 fetchModule 方法,重覆上面的邏輯。

如果是正常函數變數之類的,則收集對應 statement 。執行到最後,我們就可以把相關聯的 statement 都收集起來,未被收集到,說明其就是無用代碼,已經被過濾了。

最後在重組成 bundle,通過 fs 在發送到我們的文件。

留在最後

tree shaking 還要很多點值得挖掘,如:

  • css 的 tree shaking
  • webpack 的 tree shaking 實現
  • 如何避免 tree shaking 無效
  • ...

參考資料


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、課程介紹 數據服務API作為數據統一服務平臺建設的最上層,能夠將數據倉庫數據以服務化、介面化的方式提供給數據使用方,屏蔽底層數據存儲、計算的諸多細節,簡化和加強數據的使用。 隨著企業“互聯網化、數字化”進程的不斷深入,越來越多的業務被遷移到互聯網上,產生大量的業務交互和對外服務需求,對API介面 ...
  • 一、包的作用 • Oracle中包的概念與Java中包的概念非常類似,只是Java中的包是為了分類管理類,但是關鍵字都是package。 • 在一個大型項目中,可能有很多模塊,而每個模塊又有自己的過程、函數等。而這些過程、函數預設是放在一起的(如在PL/SQL中,過程預設都是放在一起的,即Proce ...
  • 一、android工程配置 buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.1.4' } } apply plugin: 'com.android. ...
  • 在開始集成 ZEGO Express SDK 前,請確保開發環境滿足以下要求 ...
  • reduce 的學習方法 array.reduce(callback(prev, currentValue, index, arr), initialValue) //簡寫就是下麵這樣的 arr.reduce(callback,[initialValue]) callback (執行數組中每個值的函 ...
  • 基礎類型 TypeScript 支持與 JavaScript 幾乎相同的數據類型,此外還提供了實用的枚舉類型方便我們使用。 布爾值 最基本的數據類型就是簡單的true/false值,在JavaScript和TypeScript里叫做boolean(其它語言中也一樣) let isDone: bool ...
  • 行業測試數據-案例分享 一 總體介紹 “人類正從IT時代走向DT時代” 1.數據測試指的是檢查局部數據結構時為了保證臨時存儲在模塊內的數據在程式執行過程中完整、正確的過程。2.工程師開發完成後,常常需要製造大批量的偽數據,來測試數據中台的開發效果。例如在數倉開發中,會遇到需要在已構建的數倉模型(各種 ...
  • 這裡給大家分享我在網上總結出來的一些JavaScript 知識,希望對大家有所幫助 一、日期處理 1. 檢查日期是否有效 該方法用於檢測給出的日期是否有效: const isDateValid = (...val) => !Number.isNaN(new Date(...val).valueOf( ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...