Tree Shaking

本文介绍了Treeshaking的概念及其在webpack中的应用,通过具体实例展示了如何利用Treeshaking特性减少未使用的代码,从而减小最终输出文件的大小。


前言

最近这段时间在了解并学习Vue3的新特性,在其中遇到一些概念,自己平时接触的很少,有些只是听过,有些甚至闻所未闻(主要是自己学疏才浅)。在看到Vue3与Vue2相比,在bundle包的大小上减小了41%,最主要就是运用了 Tree shaking 的特性,那么什么是 Tree shaking 呢?按照中文直译过来不就是抖树吗?既然碰到了,好歹也要了解一下,别到时候吹牛都吹不出来(吹牛是不好滴)


一、Tree shaking 是什么?

先来官方(MDN)的说法:
Tree shaking 是一个通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code) 行为的术语。

它依赖于ES2015中的 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。

在现代 JavaScript 应用程序中,我们使用模块打包(如webpack或Rollup)将多个 JavaScript 文件打包为单个文件时自动删除未引用的代码。这对于准备预备发布代码的工作非常重要,这样可以使最终文件具有简洁的结构和最小化大小。

看了官方的说法,大概的感觉就是在项目模块打包过程中,通过import和export语句,可以移除引入文件中未被使用的代码,这样就可以缩减代码量,简化代码结构。

二、webpack中 Tree shaking 的相关使用

新的 webpack 4 正式版本,扩展了这个检测能力,通过 package.json 的 “sideEffects” 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure(纯的 ES2015 模块)”,由此可以安全地删除文件中未使用的部分。既然提到了 Tree shaking 实际运用于如webpack或Rollup这样的模块打包工具,对于自己而言,webpack使用场景相对较多。所以还是了解一下webpack中 Tree shaking 相关概念,我们就结合 webpack 官网的讲解进行分析吧。

1.添加一个通用模块

在我们的项目中添加一个新的通用模块文件 math.js,此文件导出两个函数:

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

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

然后再另一个模块文件index.js中导入:

import { cube } from './math.js';
function component () {
	var element = document.createElement('pre');
	element.innerHTML = ['Hello webpack!', '5 cubed is equal to ' + cube(5)].join('\n\n');
	return element;
}
document.body.appendChild(component());

注意,我们并未从 math.js 模块中 import 导入 square 方法。这个功能是所谓的“未引用代码(dead code)”,也就是说,应该删除掉未被引用的 export。现在让我们运行我们的npm 脚本 npm run build,并检查输出的 bundle:

/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function square(x) {
  return x * x;
}

function cube(x) {
  return x * x * x;
}

注意,上面的 unused harmony export square 注释。如果你看下面的代码,你会注意到 square 没有被导入,但是,它仍然被包含在 bundle 中。我们将在下一节中解决这个问题。

2.将文件标记为无副作用(side-effect-free)

在一个纯粹的 ESM 模块世界中,识别出哪些文件有副作用很简单。然而,我们的项目无法达到这种纯度,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。
这种方式是通过 package.json 的 “sideEffects” 属性来实现的。

{
  "name": "your-project",
  "sideEffects": false
}

如同上面提到的,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export 导出。

「副作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

如果你的代码确实有一些副作用,那么可以改为提供一个数组:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
  ]
}

数组方式支持相关文件的相对路径、绝对路径和 glob 模式。它在内部使用 micromatch。

注意,任何导入的文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并导入 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js",
    "*.css"
  ]
}

最后,还可以在 module.rules 配置选项 中设置 “sideEffects”。

3.压缩输出

通过如上方式,我们已经可以通过 import 和 export 语法,找出那些需要删除的“未使用代码(dead code)”,然而,我们不只是要找出,还需要在 bundle 中删除它们。为此,我们将使用 -p(production) 这个 webpack 编译标记,来启用 uglifyjs 压缩插件。

注意,–optimize-minimize 标记也会在 webpack 内部调用 UglifyJsPlugin。
从 webpack 4 开始,也可以通过 “mode” 配置选项轻松切换到压缩输出,只需设置为 “production”。

webpack.config.js

const path = require('path');

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

准备就绪后,然后运行另一个命令 npm run build,看看输出结果有没有发生改变。
你发现 dist/bundle.js 中的差异了吗?显然,现在整个 bundle 都已经被精简过,但是如果仔细观察,则不会看到 square 函数被引入,但会看到 cube 函数的修改版本(function r(e){return eee}n.a=r)。现在,随着 tree shaking 和代码压缩,我们的 bundle 减小几个字节!虽然,在这个特定示例中,可能看起来没有减少很多,但是,在具有复杂的依赖树的大型应用程序上运行时,tree shaking 或许会对 bundle 产生显著的体积优化。

三、Tree shaking 简单测试

上面这一大段都是 webpack 官网的讲解和实例,最主要很多东西不自己玩一下,会不会感觉好像还是不那么透彻。。。这也许就是天才和学渣的区别吧。
其实现在自从前端模块化之后,我们会经常接触到 exportimport , 对于 export,我们经常会将一些方法或者对象导出,在我印象中有两种写法:

const str = 'Hello World';
const sayHello = () => {
	console.log('say Hi')
}
export default {
	str,
	sayHello
}
export const str = 'Hello World';
export const sayHello = () => {
	console.log('say Hi')
}

其实export与export default均可用于导出常量、函数、文件、模块等,你可以在其它文件或模块中通过import 将其导入,以便能够对其进行使用。
那么有人就会问,这两种有什么区别?
在一个文件或模块中,export、import可以有多个,但是export default只能有仅有一个;
通过export方式导出,在导入时要加{ },export default则不需要。

import moduleA from '@/utils/moduleA'
import { str, sayHello } from '@/utils/moduleB'
export default {
  name: 'HelloWorld',
  mounted() {
    this.sayHi_A();
    this.sayHi_B();
  },
  methods: {
    sayHi_A: function(){
      console.log('module_A str', moduleA.str);
      moduleA.sayHello();
    },
    sayHi_B: function(){
      console.log('module_B str', str);
      sayHello();
    }
  }
}

在这里插入图片描述
这是简单的演示了使用方法。

import moduleA from '@/utils/moduleA'
// import { str, sayHello } from '@/utils/moduleB'
export default {
  name: 'Test',
  mounted() {
    this.sayHi_A();
    // this.sayHi_B();
  },
  methods: {
    sayHi_A: function(){
      console.log('module_A str', moduleA.str);
      moduleA.sayHello();
    },
    /*sayHi_B: function(){
      console.log('module_B str', str);
      sayHello();
    }*/
  }
}

在这里插入图片描述
在这里插入图片描述

// import moduleA from '@/utils/moduleA'
import { str, sayHello } from '@/utils/moduleB'
export default {
  name: 'Test',
  mounted() {
    // this.sayHi_A();
    this.sayHi_B();
  },
  methods: {
    /*sayHi_A: function(){
      console.log('module_A str', moduleA.str);
      moduleA.sayHello();
    },*/
    sayHi_B: function(){
      console.log('module_B str', str);
      sayHello();
    }
  }
}

在这里插入图片描述
在这里插入图片描述

当然app.js文件默认是被压缩过格式化过,我将其Reformat Code, 更方便查看打包的内容,虽然从上面看出来压缩后分别是2.92KB和2.49KB,仿佛区别不大,主要文件中代码量较小,当代码量方法上升到框架级别,那区别应该是一目了然。
在Vue3中许多渐进式的特性都使用了第二种的写法来进行重写,而且模板本身又是Tree shaking友好的。但不是所有东西都可以被抖掉,有部分代码是对任何类型的应用程序都不可或缺的,我们把这些不可或缺的部分称之为基线大小,所以Vue3尽管增加了很多的新特性,但是被压缩后的基线大小只有10KB左右,甚至不到Vue2的一半。

总结

你可以将应用程序想象成一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。估计这就是抖树的意义吧。

### Tree Shaking 的概念与实现方式 Tree shaking 是一种优化技术,用于移除 JavaScript 代码中未使用的部分,从而减少最终打包文件的大小。它主要依赖于静态分析和模块化标准(如 ES6 模块语法)来识别哪些代码是不必要的[^3]。 #### 树形结构中的“死代码” 在 JavaScript 中,如果某些代码从未被引用或使用,则这些代码被称为“死代码”或“未使用的导出”。Tree shaking 的目标就是通过静态分析找到这些未使用的部分并将其从最终的构建输出中移除。例如,在 `helpers.js` 文件中: ```javascript // helpers.js export function foo() { return 'foo'; } export function bar() { return 'bar'; } ``` 如果在项目中只导入了 `foo` 而没有使用 `bar`,那么 Tree shaking 可以确保 `bar` 不会被包含在最终的打包文件中[^1]。 #### 工具支持 Webpack 是一个广泛使用的打包工具,支持 Tree shaking 功能。为了使 Tree shaking 生效,需要遵循以下条件: 1. **使用 ES6 模块语法**:`import` 和 `export` 是 Tree shaking 的基础。只有使用这些语法,打包工具才能正确地进行静态分析。 2. **避免副作用**:如果模块中有副作用(例如执行全局变量修改、DOM 操作等),Webpack 默认会保留整个模块。可以通过配置 `sideEffects: false` 来明确声明模块无副作用[^4]。 #### 实现步骤 以下是 Webpack 配置中启用 Tree shaking 的关键点: - **设置 mode 为 production**:在生产模式下,Webpack 会自动启用 Tree shaking 和其他优化功能。 - **配置 sideEffects**:在 `package.json` 中添加 `sideEffects` 属性,告诉 Webpack 哪些文件可以安全地进行 Tree shaking。 ```json { "sideEffects": false } ``` 或者更精细地指定哪些文件有副作用: ```json { "sideEffects": ["*.css", "*.scss"] } ``` - **代码示例** 假设我们有一个简单的项目结构,其中 `app.js` 只导入了 `foo` 函数: ```javascript // app.js import { foo } from './helpers'; console.log(foo()); ``` 经过 Tree shaking 后,`helpers.js` 的内容会被优化为: ```javascript function (t, n, r) { function e() { return "foo"; } n.foo = e; } ``` 可以看到,`bar` 函数已经被移除[^3]。 #### 注意事项 尽管 Tree shaking 是一种强大的优化工具,但在实际应用中需要注意以下几点: - **动态导入限制**:动态导入(如 `import()`)会导致无法进行静态分析,因此可能影响 Tree shaking 的效果。 - **第三方库的影响**:一些第三方库可能包含大量未使用的代码,且未明确声明无副作用。在这种情况下,手动配置 `sideEffects` 或选择性引入函数可以帮助优化[^4]。 --- ### 示例代码 以下是一个完整的 Webpack 配置示例,展示如何启用 Tree shaking: ```javascript const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: 'production', entry: './src/app.js', output: { filename: 'bundle.js', clean: true, }, optimization: { usedExports: true, // 启用 Tree shaking }, plugins: [ new HtmlWebpackPlugin({ template: './index.html', }), ], }; ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值