解锁新姿势: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 替我们做默认的优化配置,但这次自己亲自了解一些关于前端的性能优化后,感觉也满不错的😂

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

相关推荐
fat house cat_41 分钟前
volatile,原来是这么回事
java·jvm·面试·volatile
软件测试曦曦1 小时前
外包干了4年,技术退步太明显了。。。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
鱼跃鹰飞2 小时前
Leetcode面试经典150题-198.打家劫舍
数据结构·算法·leetcode·面试·职场和发展
Flying_Fish_roe3 小时前
mysql性能优化-SQL 查询优化
sql·mysql·性能优化
moisture3 小时前
C++ 值类别、auto与decltype
后端·面试
极客先躯3 小时前
高级java每日一道面试题-2024年9月15日-架构篇[分布式篇]-如何在分布式系统中实现事务?
java·数据库·分布式·面试·架构·事务·分布式篇
yanlele7 小时前
前端面试第 66 期 - Vue 专题第二篇 - 2024.09.22 更新前端面试问题总结(20道题)
前端·javascript·面试
hn小菜鸡7 小时前
LeetCode 面试经典150题 67.二进制求和
算法·leetcode·面试
江凡心7 小时前
Qt 每日面试题 -2
开发语言·数据库·qt·面试
鱼跃鹰飞8 小时前
Leetcode面试经典150题-94.二叉树的中序遍历
算法·leetcode·面试