webpack tree shaking 深入实践

477 阅读3分钟

之前对tree shaking的认识仅仅停留在无用代码剔除上。今天来深究总结一下。

什么是tree shaking

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination

Tree shaking是一个用在js中删除无用代码的术语。

在webpack 2版本中webpack内置支持了ES2015 modules,并且也支持了无用模块的导出检测。webpack 4版本在此功能上进行了扩展,并通过在package.json 中添加“ sideEffects” 属性向编译器提供提示,以标示项目中的哪些文件是“纯”的,从而可以安全的移除。

背后依据的原理

所以tree shaking背后依据的原理肯定和es module息息相关— 依赖的是ES6模块的静态分析能力,体现在:

  • Export和import 只能出现在文件代码的顶层
  • 使用 import 导入的变量是readonly的,类似const

demo实验

下面的例子都是用最新版的webpack

一个tree shaking可行的demo

// test.js:
export const one = function (value) {
  console.log("this is one");
  return value
};
export const two = function (value) {
  console.log("this is two");
  return value;
};

// index.js
import { one, two } from "./test";
console.log(two("test"));

在生产环境下打包(下同),发现函数one没有被打包进去:

WeChat340d8eb3ba34b562aec8e66085b46ee7

一个tree shaking不可行的demo

// test-one.js
window.testOne = () => {
  console.log("this is window test one")
}

export const anotherTest = () => {
  console.log("another test")
}

// test.js
import { anotherTest } from "./test-one.js";

export const one = function (value) {
  console.log("this is one");
  return value;
};
export const two = function (value) {
  console.log("this is two");
  return value;
};

// index.js
import { one, two } from "./test";
console.log(two("test"))

打包后:

WeChat97ab32591ccdc1d089b2cdbf04833e95

可以看到test-one.js 中的anotherTest虽然已经exported,因为没有被使用,所以被tree shaking了。但是,test-one.js中的window.testOne代码却被打包了。如果我想要的效果是:test-one.js export 的内容没有用到就不需要打包该怎么办呢?下面来介绍一下package.json中可以添加的一个属性sideEffects。

sideEffects

sideEffects 顾名思义是side effects,也就是副作用(函数式编程中的一个名词)。在webpack4中,通过在package.json 中添加"sideEffects": false , 可以向webpack指明整个项目是没用副作用的,可以安全的 tree-shaking 。sideEffects 也可以接受一个数组,数组的每一项是文件路径,用于保留这些文件的副作用:

"sideEffects":["./src/global.config.js"]

用sideEffects解决第二个demo的问题

在package.json中配置"sideEffects": false 后,我们再来打包看一下结果: WeChat67144c387c48827c058d21959fddffdb 我们可以看到window.testOne确实没有被打包了

sideEffects 的注意点

有时候在项目中我们确实想“引入一些有副作用的文件”,比如我们想在window对象上定义一些js函数,供native端调用 (js bridge):

 // global.js
window.NativeBridge = {
  share: () => {
    console.log("this is native share")
  }
}

此时我们在index.js中调用global.js

import { one, two } from "./test";
import "./global.js"

console.log(two("test"));

打包后发现global.js根本没有被打包进文件。导致这种错误的原因是:我们是通过import "./global.js" 引入文件的,webpack 会把所有import "xxx" 看做是引入了文件,但是没有使用的。 如果此时在package.json中配置"sideEffects": false ”, 那么就global.js会被 tree-shaking 。

解决中类似import "xxx" 的方法是在`"sideEffects”中表明有副作用的文件:

"sideEffects":["./src/global.js"]

Tree-shaking 目前的局限

直接上代码:

  // test.js
import { isDate } from "lodash-es"

export const one = function (value) {
  console.log("this is one");
  return isDate(value);
};

export const two = function (value) {
  console.log("this is two");
  return value;
};

// index.js
import { one, two } from "./test";
import "./global.js"

console.log(two("test"));

打包后: WeChat24790fee69486bc61652b58b3fc0d7d3 可以看到虽然函数one没有没使用,但是lodash-es的部分代码还是被打包了。

解决方法:

  1. 把one函数修改为纯函数,用到的lodash的isDate方法作为依赖注入:
export const one = function (value, isDate ) {
  console.log("this is one");
  return isDate(value);
};
  1. 使用第三方webpack插件: webpack-deep-scope-analysis-plugin

关于babel

为了确保babel不会将代码编译为commonjs,配置 Babel preset @babel/preset-env 的modules属性为false

总结

想要在webpack 中更好的使用tree shaking,那么

  1. 需要使用ES6 模块
  2. 在使用babel的时候,不能编译转化为非es6模块
  3. 合理的加sideEffects。其实sideEffects就是来通知webpack可以安全的进行tree-shaking的,如果有些包真的是有副作用, 那么也可以在sideEffects中配置。
  4. 在写代码时尽量考虑到副作用的产生,合理避免。

参考:

Webpack4: Tree-shaking 深度解析

Tree Shaking