一、先搞懂Tree Shaking到底是啥

很多人用Webpack打包项目时,会发现打包出来的文件比自己写的代码加起来还大,明明有些代码根本没用到,为啥还会被打包进去?这时候就会提到一个叫Tree Shaking的功能,它的作用就是把没用到的“死代码”从打包结果里删掉,让最终文件变小。但不少人会遇到Tree Shaking不管用的情况,接下来就拆解为啥会失效,以及怎么解决。

举个最简单的例子,假设我们写了一个工具文件,里面有两个函数,一个是计算加法的,一个是计算减法的,然后在项目里只用到了加法函数,正常情况下Tree Shaking应该把减法函数删掉,只打包加法函数。但很多时候减法函数还是会出现在打包结果里,这就是Tree Shaking失效了。

先明确一下,Tree Shaking的核心逻辑是“静态分析”,也就是Webpack在打包的时候,提前把所有代码的依赖关系理清楚,找出哪些代码是“死代码”(没有被任何地方引用的代码),然后把这些代码删掉。所以只要静态分析的逻辑走不通,Tree Shaking就会失效。

二、Tree Shaking失效的常见原因

2.1 模块类型不匹配

Webpack默认的Tree Shaking只支持ES模块(也就是ES6的import/export语法),如果用的是CommonJS模块(require/module.exports),那Webpack的静态分析会失效,因为CommonJS是动态加载的,Webpack没法提前知道你会用哪个模块。

举个例子,假设我们有两个文件,一个是用CommonJS写的工具文件,一个是项目入口文件: 【技术栈:JavaScript + Webpack】

// utils.js (CommonJS模块)
// 加法函数
function add(a, b) {
  return a + b;
}
// 减法函数
function subtract(a, b) {
  return a - b;
}
// 导出两个函数
module.exports = {
  add,
  subtract
};
// index.js (入口文件)
// 只导入加法函数
const { add } = require('./utils');
console.log(add(1, 2));

这种情况下,Webpack打包后,subtract函数还是会被打包进去,因为CommonJS的动态特性让Webpack没法判断subtract有没有被用到。

2.2 代码有副作用

什么是副作用?就是一段代码执行的时候,除了返回值之外,还会对外部环境产生影响,比如修改全局变量、触发网络请求、调用console.log等。如果一段代码有副作用,哪怕没有被引用,Webpack也不敢随便删掉它,因为怕删掉后会影响程序的正常运行。

举个例子,假设我们的工具文件里有一个函数,执行的时候会修改全局变量: 【技术栈:JavaScript + Webpack】

// utils.js (ES模块)
// 加法函数
export function add(a, b) {
  return a + b;
}
// 减法函数,有副作用(修改全局变量)
export function subtract(a, b) {
  // 修改全局变量window.count
  window.count = 100;
  return a - b;
}
// index.js (入口文件)
// 只导入加法函数
import { add } from './utils';
console.log(add(1, 2));

这种情况下,subtract函数因为有副作用,哪怕没有被引用,Webpack也不会删掉它,因为怕删掉后window.count的修改逻辑没了,程序出问题。

2.3 Webpack配置不对

很多人用Webpack的时候,会忽略一些和Tree Shaking相关的配置,比如mode、optimization的配置,这些配置不对的话,Tree Shaking也会失效。

比如,Webpack的mode如果设置为development,默认不会开启Tree Shaking,因为开发环境需要快速打包,不需要优化;只有设置为production的时候,才会默认开启Tree Shaking。

再比如,optimization.usedExports这个配置,它的作用是告诉Webpack标记出那些没有被用到的导出,然后交给Tree Shaking处理。如果这个配置没开,Webpack不会标记死代码,Tree Shaking自然也没法工作。

2.4 第三方库的问题

很多时候我们会用到第三方库,比如lodash,这些库如果没有按照ES模块的规范编写,或者本身有很多副作用,也会导致Tree Shaking失效。

比如lodash的旧版本,很多是CommonJS模块,或者打包的时候把所有函数都打包到一个文件里,没有做ES模块的拆分,这种情况下Tree Shaking没法把没用到的函数删掉。

举个例子,假设我们只用到lodash的cloneDeep函数,如果直接导入整个lodash库: 【技术栈:JavaScript + Webpack】

// index.js (入口文件)
// 导入整个lodash库
import _ from 'lodash';
// 只用到cloneDeep函数
const obj = { a: 1 };
const newObj = _.cloneDeep(obj);
console.log(newObj);

这种情况下,整个lodash库都会被打包进去,哪怕只用到了一个函数,因为Webpack没法从整个库的代码里找出哪些是没用到的。

三、解决Tree Shaking失效的方法

3.1 统一使用ES模块

如果项目里还有CommonJS模块,尽量改成ES模块的import/export语法,这样Webpack才能进行静态分析。

比如把之前的CommonJS例子改成ES模块: 【技术栈:JavaScript + Webpack】

// utils.js (ES模块)
// 加法函数
export function add(a, b) {
  return a + b;
}
// 减法函数
export function subtract(a, b) {
  return a - b;
}
// index.js (入口文件)
// 只导入加法函数
import { add } from './utils';
console.log(add(1, 2));

这种情况下,Webpack打包后,subtract函数就会被删掉,因为Webpack能通过静态分析知道它没有被用到。

3.2 处理代码的副作用

如果代码确实有副作用,但我们确定这些副作用不会影响程序运行,可以用sideEffects配置告诉Webpack哪些代码是没有副作用的,或者哪些文件是没有副作用的。

sideEffects配置有两种用法:一种是在package.json里配置,一种是在Webpack的module.rules里配置。

比如,我们可以在package.json里配置sideEffects: 【技术栈:JavaScript + Webpack】

{
  "name": "test-project",
  "version": "1.0.0",
  "sideEffects": [
    "*.css", // 样式文件有副作用,不能删掉
    "*.scss",
    "*.sass"
  ]
}

这个配置的意思是,除了css、scss、sass文件之外,其他文件都是没有副作用的,Webpack可以放心删掉没用到的代码。

如果我们确定某个文件没有副作用,也可以在Webpack的module.rules里配置: 【技术栈:JavaScript + Webpack】

// webpack.config.js
module.exports = {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader',
        sideEffects: false // 这个规则下的js文件没有副作用
      }
    ]
  }
};

需要注意的是,sideEffects配置要谨慎使用,如果配置错了,把有副作用的代码当成无副作用的删掉,会导致程序出错。

3.3 正确配置Webpack

要开启Tree Shaking,Webpack的配置需要注意以下几点:

  1. mode设置为production:生产环境默认开启Tree Shaking。
  2. 开启optimization.usedExports:这个配置在production模式下默认开启,但如果是自定义配置,需要手动开启。
  3. 不要禁用tree shaking:有些配置会禁用Tree Shaking,比如optimization.treeShaking设置为false,要确保这个配置是开启的。

举个正确的Webpack配置例子: 【技术栈:JavaScript + Webpack】

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './index.js', // 入口文件
  output: {
    path: path.resolve(__dirname, 'dist'), // 输出目录
    filename: 'bundle.js' // 输出文件名
  },
  mode: 'production', // 生产环境,开启Tree Shaking
  optimization: {
    usedExports: true, // 标记未使用的导出
    treeShaking: true // 开启Tree Shaking
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
};

3.4 优化第三方库的使用

对于第三方库,尽量选择支持ES模块的版本,比如lodash-es,它是lodash的ES模块版本,专门为Tree Shaking优化的。

比如之前的lodash例子,改成导入lodash-es: 【技术栈:JavaScript + Webpack】

// index.js (入口文件)
// 导入lodash-es的cloneDeep函数
import cloneDeep from 'lodash-es/cloneDeep';
// 只用到cloneDeep函数
const obj = { a: 1 };
const newObj = cloneDeep(obj);
console.log(newObj);

这种情况下,Webpack只会把cloneDeep函数打包进去,不会打包整个lodash库,因为lodash-es是ES模块,每个函数都是单独的导出,Webpack可以通过静态分析只打包用到的函数。

另外,也可以用babel-plugin-lodash这个插件,它会自动把lodash的导入改成只导入用到的函数,优化Tree Shaking的效果。

四、Tree Shaking的应用场景、优缺点和注意事项

4.1 应用场景

Tree Shaking主要应用在前端项目的打包优化场景,尤其是大型项目,比如后台管理系统、电商网站等,这些项目会用到很多第三方库和自己写的工具函数,Tree Shaking可以有效减少打包后的文件体积,提高页面加载速度。

另外,Tree Shaking也可以应用在Node.js项目的打包场景,比如用Webpack打包Node.js的服务端代码,减少服务端代码的体积,提高服务端的启动速度。

4.2 优缺点

Tree Shaking的优点很明显:

  1. 减少打包体积:删掉没用到的死代码,让最终的文件变小,提高页面加载速度,节省用户的流量。
  2. 优化代码质量:强制开发者只导入用到的代码,避免导入整个库,让代码结构更清晰。

Tree Shaking的缺点也有:

  1. 依赖静态分析:只支持ES模块,对CommonJS模块支持不好,限制了项目的模块选择。
  2. 副作用处理复杂:如果代码有副作用,需要手动配置sideEffects,配置不当会导致程序出错。
  3. 打包时间增加:静态分析需要额外的时间,大型项目的打包时间会有所增加。

4.3 注意事项

  1. 谨慎使用sideEffects配置:一定要确定代码没有副作用再配置,否则会导致程序出错。
  2. 注意第三方库的模块类型:尽量选择支持ES模块的第三方库,避免Tree Shaking失效。
  3. 开发环境不要开启Tree Shaking:开发环境需要快速打包,Tree Shaking会增加打包时间,影响开发效率。
  4. 测试Tree Shaking的效果:打包后可以用webpack-bundle-analyzer这个工具分析打包结果,检查Tree Shaking是否生效,有没有没删掉的死代码。

五、总结

Tree Shaking是Webpack中非常重要的打包优化功能,它的核心是静态分析,只有满足模块类型匹配、代码无副作用、Webpack配置正确、第三方库优化这几个条件,Tree Shaking才能正常工作。

遇到Tree Shaking失效的情况,我们可以按照以下步骤排查:首先检查项目的模块类型,是不是都用了ES模块;然后检查代码有没有副作用,有没有正确配置sideEffects;接着检查Webpack的配置,是不是设置为production模式,有没有开启usedExports和treeShaking;最后检查第三方库,是不是用了支持ES模块的版本,有没有正确导入。

只要掌握了这些原理和方法,就能解决大部分Tree Shaking失效的问题,有效减少打包体积,提高项目的性能。