最近開了一個讀者回饋表單郵箱,無論是對文章的感想或是對部落格的感想,有什麼想回饋的都可以發郵箱跟我說:i_kkkp@163.com

Webpack 中的 sideEffects 该怎么用

webpack v4 开始新增了一个 sideEffects 特性,通过给 package.json 加入 sideEffects 声明该 包/模块 是否包含 sideEffects(副作用),从而可以为 tree-shaking 提供更大的优化空间。

基于我们对 side effect 的常规理解,我们可以认为,只要我们确定当前包里的模块不包含副作用,然后将发布到 npm 里的包标注为 sideEffects: false ,我们就能为使用方提供更好的打包体验。原理是 webpack 能将标记为 side-effects-free 的包由 import {a} from xx 转换为 import {a} from 'xx/a',从而自动修剪掉不必要的 import,作用同 babel-plugin-import

Tree Shaking 与副作用

Tree-Shaking在前端界由rollup首先提出并实现,后续webpack在2.x版本也借助于UglifyJS实现了。自那以后,在各类讨论优化打包的文章中,都能看到Tree-Shaking的身影。

Tree-Shaking的原理

ES6的模块引入是静态分析的,故而可以在编译时正确判断到底加载了什么代码。

分析程序流,判断哪些变量未被使用、引用,进而删除此代码。

很好,原理非常完美,那为什么有时候我们项目里面多余的的代码又删不掉呢?

先说原因:都是副作用的锅!

副作用

了解过函数式编程的同学对副作用这词肯定不陌生。它大致可以理解成:一个函数会、或者可能会对函数外部变量产生影响的行为。

举个例子,比如这个函数:

function go (url) {
  window.location.href = url
}

这个函数修改了全局变量location,甚至还让浏览器发生了跳转,这就是一个有副作用的函数。

// componetns.js
export class Person {
  constructor ({ name }) {
    this.className = 'Person'
    this.name = name
  }
  getName () {
    return this.name
  }
}
export class Apple {
  constructor ({ model }) {
    this.className = 'Apple'
    this.model = model
  }
  getModel () {
    return this.model
  }
}
// main.js
import { Apple } from './components'

const appleModel = new Apple({
  model: 'IphoneX'
}).getModel()

console.log(appleModel)

很显然这个Person类是无用的代码

而为什么有时候别的工具,比如rollup在线repl尝试了下tree-shaking,也确实删掉了无用的代码

而使用webpack打包工具却不能进行有效的代码消除呢?

答案是:babel编译 + webpack打包

在这边贴一个链接,是有关于详细介绍babel编译 + webpack打包是怎么让你无效的代码消除不掉的。你的Tree-Shaking并没什么卵用

如果不想看文章的话,这边直接简单说一下原理:babel编译会使得Person类被封装成了一个IIFE(立即执行函数),然后返回一个构造函数,在这边就产生了一个副作用。

这边有个Issues,IIFE 中的类声明被视为副作用

当我在 IIFE 中声明一个类,但没有使用类时,它不会被 UglifyJS 剥离,因为它被认为是副作用。

var V6Engine = (function () {
    function V6Engine() {
    }
    V6Engine.prototype.toString = function () {
        return 'V6';
    };
    return V6Engine;
}());

编译时收到这样的警告:WARN: Side effects in initialization of unused variable V6Engine [./dist/car.bundle.js:74,4]

下面给出的回复:Uglify 没做执行程序流分析。它并不会因为你注意到的副作用而删除代码。你要是想弄个完善一点的摇树,去隔壁rollup呗!

issue中总结下几点关键信息:

函数的参数若是引用类型,对于它属性的操作,都是有可能会产生副作用的。因为首先它是引用类型,对它属性的任何修改其实都是改变了函数外部的数据。其次获取或修改它的属性,会触发getter或者setter,而gettersetter是不透明的,有可能会产生副作用。

uglify没有完善的程序流分析。它可以简单的判断变量后续是否被引用、修改,但是不能判断一个变量完整的修改过程,不知道它是否已经指向了外部变量,所以很多有可能会产生副作用的代码,都只能保守的不删除。

rollup有程序流分析的功能,可以更好的判断代码是否真正会产生副作用。

但这已经是很久之前的版本问题,现在的webpack tree shaking已经做了很多的优化,足够的程序流分析进行tree shaking

webpack 的 tree shaking 的作用是可以将未被使用的 exported member 标记为 unused 同时在将其 re-export 的模块中不再 export。说起来很拗口,看代码:

// a.js
export function a() {}
// b.js
export function b(){}
// package/index.js
import a from './a'
import b from './b'
export { a, b }
// app.js
import {a} from 'package'
console.log(a)

当我们以 app.js 为 entry 时,经过摇树后的代码会变成这样:

// a.js
export function a() {}
// b.js 不再导出 function b(){}
function b() {}
// package/index.js 不再导出 b 模块
import a from './a'
import './b'
export { a }
// app.js
import {a} from 'package'
console.log(a)

配合 webpack 的 scope hoistinguglify 之后,b 模块的痕迹会被完全抹杀掉。

但是如果 b 模块中添加了一些副作用,比如一个简单的 log:

// b.js
export function b(v) { reutrn v }
console.log(b(1))
webpack 之后会发现 b 模块内容变成了:

// b.js
console.log(function (v){return v}(1))

虽然 b 模块的导出是被忽略了,但是副作用代码被保留下来了。

由于目前 transformer 转换后可能引入的各种奇怪操作引发的副作用,很多时候我们会发现就算有了 tree shaking 我们的 bundle size 还是没有明显的减小。

而通常我们期望的是 b 模块既然不被使用了,其中所有的代码应该不被引入才对。

这个时候 sideEffects 的作用就显现出来了:如果我们引入的 包/模块 被标记为 sideEffects: false 了,那么不管它是否真的有副作用,只要它没有被引用到,整个 模块/包 都会被完整的移除。

以 mobx-react-devtool 为例,我们通常这样去用:

import DevTools from 'mobx-react-devtools';

class MyApp extends React.Component {
  render() {
    return (
      <div>
        ...
        { process.env.NODE_ENV === 'production' ? null : <DevTools /> }
      </div>
    );
  }
}

这是一个很常见的按需导入场景,然而在没有 sideEffects: false 配置时,即便 NODE_ENV 设为 production ,打包后的代码里依然会包含 mobx-react-devtools 包,虽然我们没使用过其导出成员,但是 mobx-react-devtools 还是会被 import,因为里面“可能”会有副作用。

但当我们加上 sideEffects false 之后,tree shaking 就能安全的把它从 bundle 里完整的移除掉了。

sideEffects 的使用场景

上面也说到,通常我们发布到 npm 上的包很难保证其是否包含副作用(可能是代码的锅可能是 transformer 的锅),但是我们基本能确保这个包是否会对包以外的对象产生影响,比如是否修改了 window 上的属性,是否复写了原生对象方法等。如果我们能保证这一点,其实我们就能知道整个包是否能设置 sideEffects: false了,至于是不是真的有副作用则并不重要,这对于 webpack 而言都是可以接受的。

这也就能解释为什么能给 vue 这个本身充满副作用的包加上 sideEffects: false 了。

所以其实 webpack 里的 sideEffects: false 的意思并不是我这个模块真的没有副作用,而只是为了在摇树时告诉 webpack:我这个包在设计的时候就是期望没有副作用的,即使他打完包后是有副作用的。

301/302-重定向 Webpack 性能优化-2

評論