解锁新姿势:Webpack Tree Shaking带来的惊人性能提升

前言

webpack 我们每天都在用,但是Tree Shaking知道多少呢,今天就来唠一唠他都帮我们干了些什么?

名词解释: Tree Shaking 是一种用于精确打包 JavaScript 应用程序的优化技术,它通过静态分析的方式,识别并删除应用程序中未使用的代码,从而减小打包后的文件大小。这个技术主要应用于基于模块化的应用程序,其中使用了 ES6 模块系统(import/export)。

Tree Shaking 是一种优化方式,在 JavaScript 中用来表示移除无用代码的常见术语。它的名字似乎源自于"当你用力摇晃一棵树时,树上只会保留绿叶,其他枯叶都会落到地上"。而那些绿叶就是打包后的文件中真正有用到的代码

在使用Tree Shaking时需要注意的是,它只能应用于静态结构(例如:import和export),而动态结构如require则无法被检测到。举个例子,如果要使用import来加载某个模块,就必须将其放在文件的最顶部,但是require可以在任何地方使用。例如下面的场景就必须等到运行时才能知道模块是什么:

js 复制代码
let module = null
if(Math.random() * 10 > 5){
  module = require('module1');
}else{
  module = require('module2');
}

在深入了解Tree Shaking的工作原理之前,可能有些同学可能会好奇,即使自己从未在Webpack中特别设置Tree Shaking,但无用的代码也会被移除啊!

那是因为执行 Tree Shaking 需要 ModuleConcatenationPlugin(图一),而 Webpack 里另外有个 mode,如果你一直没有特别去设置 mode 的值,那 mode 就预设会是 production(图二),然后 production 的预设选项中就会开启 ModuleConcatenationPlugin(也是图二),因此平常不会特别注意到也不奇怪,因为 Webpack 都帮你做好了

Tree Shaking 的运行

因为 Production 会帮你打开 ModuleConcatenationPlugin 的关系,所以我们就初始化一个简单的项目,把 mode 改成 none(按文档 none 为关闭所有优化设置),一起来玩一玩看

首先,在 src 目录下创建一个 math.js 和 string.js 文件,然后分别编写导出方法,分别是 add 和 composeString:

js 复制代码
//math.js
const add = (a, b) => a + b;

export default { add };
js 复制代码
//string.js
const composeString = (a, b) => `${a} ${b}`;

export default { composeString };

打开 src 目录下的 index.js 文件,将 add 和 composeString 两个方法都导入进来,但只调用 add 方法:

js 复制代码
//index.js
import { add } from './math';
import { addString } from './string';

console.log(add(1, 2))

最后,在终端中运行 npm run build 或者 webpack 进行打包。打包完成后,我们会发现尽管我们只使用了 import add,但打包后的文件内容仍然会包含 composeString

不过这很正常,毕竟我们还没有进行任何处理,Webpack 在打包时也不知道你到底有没有使用到哪些代码,所以无法帮你移除 composeString

那么到底什么样的代码是有用的,怎样是没用的呢?

  1. 最明显的定义应该是,如果有被执行就代表有用到。就像上面的案例中的 add 一样
  2. 有副作用的代码也是被使用的。就像上面的 index.js,看起来没有提供任何方法,但在执行时会在控制台中留下日志。此外,改变执行环境的 polyfill 也是有副作用的库。

第一种情况相对容易分辨,但如果是第二种情况的话,可以选择使用 Webpack 中的 sideEffects 属性来设置

副作用

sideEffects 可以被设置为布尔值或数组。当你将其设置为 false 时,表示该项目没有副作用,即全部使用 export 来判断是否使用。此外,sideEffects 还依赖于 providedExports,用于找出项目中所有 export 的模块

以下是 sideEffects 的使用方式:

json 复制代码
//package.json
{
  "name": "tree-shaking",
  "sideEffects": false,
  "version": "1.0.0",
  ...
}

只要在 package.json 文件中添加 sideEffects,并将其值设置为 false,就表示该项目中的所有代码都没有副作用,因此在 Webpack 打包时,可以移除未使用的导出代码。

加上 sideEffects 后打包,就看不到 composeString 在结果里了:

现在我们再去src目录下创建一个polyfill.js文件,在polyfill.js中为Array对象添加自定义的方法,然后将它导入到index.js文件中:

js 复制代码
//index.js
import './polyfill';
import { add } from './math';
import { addString } from './string';

console.log([].customMethod());
js 复制代码
//polyfill.js
Array.prototype.customMethod = () => {
  console.log('customMethods');
};

如果我们打包上方的代码,polyfill.js 就不会被提供的导出项 providedExports 捕捉到,因为它没有任何导出项,所以也不会被打包到生产环境中。这将导致项目中使用了 Array 的 customMethod 在运行时出错。面对这种情况,我们必须在 sideEffects 属性中声明 polyfill.js 具有副作用。设置方法如下:

json 复制代码
{
  "name": "tree-shaking",
  "sideEffects": ["./src/polyfill.js"],
  "version": "1.0.0",
  ...
}

这样一来,polyfill.js 就会直接被打包了:

最后要注意两件事情:

  1. 如果你们的项目中也使用了 import .css 样式文件的话,记得将以 .css 结尾的文件放到 sideEffects 中,例如 sideEffects: ["*.css"]
  2. 在 webpack.config.js 文件中的 optimization 部分也有一个 sideEffects 属性,但是这里设置的值是针对 node_modules 目录下的模块。

使用导出的

useExported 和 sideEffects 的作用都是用来判断是否应该移除代码,但根据 Webpack 文档内的说明,useExported 才是真正的 Tree Shaking:

usedExports会使用terser判断代码有没有副作用,如果没有使用到,并且没有副作用的话,在打包时会标记为unused harmony,并在压缩(使用Uglifyjs或其他工具)时移除

在测试 usedExports 之前,先到 math.js 里加入 square 并 export:

js 复制代码
//math.js
const add = (a, b) => a + b;

const square = (a, b) => a * b;

export { add, square };

现在 webpack.config.js 文件中添加 optimization.usedExports

json 复制代码
//webpack.config.js
module.exports = {
  ...
  optimization: {
    usedExports: true,
  }
};

然后对项目进行打包,你会发现只是导出(export),但是没有使用的 square 会被标记为未使用的 harmony export:

然后我们使用 uglifyjs-webpack-plugin,将未使用的 square 从树上摇晃下来:

js 复制代码
npm install -d uglifyjs-webpack-plugin

webpack.config.js 的设置如下:

js 复制代码
module.exports = { 
  entry: './src/index.js', 
  output: { 
     path: __dirname + '/dist', 
     filename: 'bundle.js' 
  }, 
  module: { 
     rules: [
       { 
         test: /\.js$/, exclude: /node_modules/, 
         use: { 
           loader: 'babel-loader', 
           options: { 
              presets: ['@babel/preset-env'] 
           } 
         } 
      }, 
      { 
         test: /\.css$/, 
         use: ['style-loader', 'css-loader'] 
      }, 
      { 
         test: /\.(png|svg|jpg|gif)$/, 
         use: ['file-loader'] 
      } 
    ] 
  } 
}; 

以上是 webpack.config.js 的设置。

js 复制代码
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  ...
  optimization: {
    usedExports: true,
    minimize: true,
    minimizer: [
        new UglifyJsPlugin({
            uglifyOptions: {
                compress: { unused: true },
                mangle: false,
                output: {
                    beautify: true
                }
            },
        })
    ],
  }
};

在设置完minimizer之后,再次打包一次,就能看到square已经被移除了:

与 sideEffects 不同的是,usedExports 可以以陈述句为单位去判断是否有 side effect,但是 sideEffects 可以让 Webpack 在打包的时候,直接略过一整个文件,只要是出现在 sideEffect 里的文件就是直接打包,也不用通过 terser 评估副作用

总结

  1. Tree Shaking 只能在静态结构中使用。如果项目中的 Babel 将静态结构编译为动态结构,需要进行额外的设置
  2. 在使用 sideEffects 时,需要将其写入 package.json 文件中。如果要对第三方库进行优化,则需要将其写入 webpack.config.js 文件中的 optimization 部分。
  3. usedExports 才是 Tree Shacking,使用时会自动判断没使用的代码,并标记 unused harmony 的注解,要移除的话要另外使用 minify

虽然平常都是直接使用 Webpack 的 Production 替我们做默认的优化配置,但这次自己亲自了解一些关于前端的性能优化后,感觉也满不错的😂

如果您在文章中发现任何问题,请告诉我,我会尽快回复并进行修正。非常感谢您的配合和理解。🙇‍♂️

相关推荐
无尽的大道4 小时前
Java反射原理及其性能优化
jvm·性能优化
理想不理想v5 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
测试19985 小时前
2024软件测试面试热点问题
自动化测试·软件测试·python·测试工具·面试·职场和发展·压力测试
马剑威(威哥爱编程)6 小时前
MongoDB面试专题33道解析
数据库·mongodb·面试
独行soc8 小时前
#渗透测试#SRC漏洞挖掘#深入挖掘XSS漏洞02之测试流程
web安全·面试·渗透测试·xss·漏洞挖掘·1024程序员节
萌面小侠Plus8 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
理想不理想v8 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
sszmvb12349 小时前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺10 小时前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉10 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试