简介

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码 (dead-code)。
如同英文名“树摇”,将一棵树上废弃的叶子摇落,使整体看起来更整洁。
它的实现依赖于 ES2015 模块系统中的静态结构特性,例如 importexport
这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup
这意味着tree shaking只支持ES module的语法,即import、export这种,而不支持require这种commonJS模块化语法。

优势

最大的好处便是能够减少打包后的项目体积。
如果你的项目中有大量未引用代码,此时 _tree shakin_g 的优势会很明显。反之,那么作用不明显。

发展

版本 支持程度
webpack2 正式版本 内置支持 ES2015 模块(也叫做 harmony 模块)和未引用模块检测能力。
webpack4 正式版本 扩展了这个检测能力,通过 package.json"sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure(纯的 ES2015 模块)”,由此可以安全地删除文件中未使用的部分。

拓展

什么样的代码属于未引用代码(dead-code)?

简单地说,就是 Webpack 没看到你使用的代码。
Webpack 会跟踪监测应用程序的 import/export 语句,如果导入的类库/函数最终没有被使用便会被认定为未引用的代码,并会对其进行 tree-shaking 。
可以通过下面的例子来理解:

  1. // 导入并赋值给 JavaScript 对象,然后在下面的代码中被用到
  2. // 这会被看作“活”代码,不会做 tree-shaking
  3. import Stuff from './stuff';
  4. doSomething(Stuff);
  5. // 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
  6. // 这就会被当做“死”代码,会被 tree-shaking
  7. import Stuff from './stuff';
  8. doSomething();
  9. // 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
  10. // 这会被当做“死”代码,会被 tree-shaking
  11. import './stuff';
  12. doSomething();
  13. // 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
  14. // 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对 库的导入 和 本地代码导入 的处理方式不同。
  15. import 'my-lib';
  16. doSomething();

由于Eslint的大面积使用,现代化的项目中出现未引用代码的情况较少。

什么样的代码属于有副作用代码(side-effects)?

仅仅因为 Webpack 看不到一段正在使用的代码,并不意味着它可以安全地进行 tree-shaking。
有些模块导入,只要被引入,就会对应用程序产生重要的影响。
比如 定义了全局样式的CSS文件 或者 包含有全局配置的JS文件

Webpack 认为这样的文件有“副作用”。具有副作用的文件不应该做 tree-shaking,因为这将破坏整个应用程序。
Webpack 的设计者清楚地认识到不知道哪些文件有副作用的情况下打包代码的风险,因此默认地将所有代码视为有副作用。
这可以保护你免于删除必要的文件,但这意味着 Webpack 的默认行为实际上是不进行 tree-shaking。

我们可以配置我们的项目,告诉 Webpack 它是没有副作用的,可以进行 tree-shaking。

怎样导入类库才支持树摇?

我们需要用支持 tree-shaking 的方式导入类库(import)。在编写支持 tree-shaking 的代码时,导入方式非常重要。
你应该避免将整个库导入到单个 JavaScript 对象中。当你这样做时,你是在告诉 Webpack 你需要整个库, Webpack 就不会摇它。
以流行的库 Lodash 为例。一次导入整个库是不支持 tree shaking 的。

  1. // 全部导入 (不支持 tree-shaking)
  2. import _ from 'lodash';
  3. // 具名导入(支持 tree-shaking)
  4. import { debounce } from 'lodash';
  5. // 直接导入具体的模块 (支持 tree-shaking)
  6. import debounce from 'lodash/lib/debounce';

配置

Webpack配置文件

JS树揺配置

使用 Webpack 进行 tree-shaking 的第一步是编写 Webpack 配置文件。
如果你想要对代码进行 tree-shaking,就需要以下几项:

首先,你需要处于生产模式。
Webpack4 只有在压缩代码的时候会 tree-shaking,而这只会发生在生产模式中。

其次,必须将优化选项 “usedExports” 设置为true。
这意味着 Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记。

最后,你需要使用一个支持删除死代码的压缩器(插件)。
这种压缩器将识别出 Webpack 是如何标记它认为没有被使用的代码,并将其剥离。
支持树摇的插件很多,比如 Uglifyjs-webpack-plugin、TerserPlugin。

在不同的开发模式环境下有不同的配置启用方法。

  1. // production mode
  2. const config = {
  3. mode: 'production',
  4. optimization: {
  5. usedExports: true,
  6. minimizer: [
  7. new TerserPlugin({...})
  8. ]
  9. }
  10. };

CSS树揺配置

如果要Shaking掉CSS中用不到的样式定义,需要借助webpack的 purifycss-webpack 插件来实现。

  1. const path = require('path');
  2. const glob = require('glob');
  3. const ExtractTextPlugin = require('extract-text-webpack-plugin');
  4. const PurifyCSSPlugin = require('purifycss-webpack');
  5. module.exports = {
  6. entry: {...},
  7. output: {...},
  8. module: {
  9. rules: [
  10. {
  11. test: /\.css$/,
  12. loader: ExtractTextPlugin.extract({
  13. fallbackLoader: 'style-loader',
  14. loader: 'css-loader'
  15. })
  16. }
  17. ]
  18. },
  19. plugins: [
  20. new ExtractTextPlugin('[name].[contenthash].css'),
  21. // Make sure this is after ExtractTextPlugin!
  22. new PurifyCSSPlugin({
  23. // Give paths to parse for rules. These should be absolute!
  24. paths: glob.sync(path.join(__dirname, 'app/*.html')),
  25. })
  26. ]
  27. };

全局CSS副作用标记

如果我们通过 import 语句在上下文中定义了全局CSS,示例如下:

  1. // 导入全局 CSS
  2. import './MyStylesheet.css';

在 webpack 构建时树摇操作会将上述方式导入的任何样式表现在都将从输出中删除,因为会被认定为未引用的代码片段。
有一个简单的解决方案可以解决这个问题。
Webpack 使用它的模块规则系统来控制各种类型文件的加载。每种文件类型的每个规则都有自己的 sideEffects 标志。这会覆盖之前为匹配规则的文件设置的所有 sideEffects 标志。
所以,为了保留全局 CSS 文件,我们只需要设置这个特殊的 sideEffects 标志为 true,如下面例子:

  1. // 全局 CSS 副作用规则相关的 Webpack 配置
  2. const config = {
  3. module: {
  4. rules: [
  5. {
  6. test: /regex/,
  7. use: [loaders],
  8. sideEffects: true
  9. }
  10. ]
  11. }
  12. };

Webpack 的所有模块规则上都有这个属性。处理全局样式表的规则必须用上它,包括但不限于 CSS/SCSS/LESS/等等。

npm配置文件

副作用标记

package.json 有一个特殊的属性 sideEffects,这个属性我们可以用来标记有副作用的代码。
它的值可以是布尔值或数组:

解释
true 默认值。这意味着所有的文件都有副作用,也就是没有一个文件可以 tree-shaking。
false 项目中没有文件有副作用,所有文件都可以进行 tree-shaking。
[…filepath] 文件路径数组。它告诉 webpack,除了数组中包含的文件外,你的任何文件都没有副作用。
因此,除了指定的文件之外,其他文件都可以安全地进行 tree-shaking。

每个项目都必须将 sideEffects 属性设置为 false 或文件路径数组。

  1. // 所有文件都有副作用,全都不可 tree-shaking
  2. {
  3. "sideEffects": true
  4. }
  5. // 没有文件有副作用,全都可以 tree-shaking
  6. {
  7. "sideEffects": false
  8. }
  9. // 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
  10. {
  11. "sideEffects": [
  12. "./src/file1.js",
  13. "./src/file2.js"
  14. ]
  15. }

参考链接 https://juejin.im/post/5dcec27d5188254b0147e619 https://www.yuque.com/dante/rd660c/treeshaking