Tree Shaking

TL;DR;

  • 前端打包中会将多个module打包成一个bundle发给客户端,但是各个module中存在并不会使用到的东西,dead code elimination是一种可以消除这些代码的技术
  • Tree Shaking是Dead code elimination(简称DCE)的一种实现
  • Webpack中的tree shaking需要es6的import/export的支持
  • Webpack中的tree shaking仅仅能作用与顶层函数、顶层变量、顶层对象,如果定义一个函数集合的Object,如果仅仅只使用了一部分函数,其他的函数仍然会被打进去

    什么是Tree Shaking

    在前端开发的过程中,一般整个代码会分散在项目的各个模块中,但是在最后在发送给客户端的时候,往往是一个大的bundle文件,各个模块的文件打包成一个JS文件,但是伴随而来的问题就是,某些永远跑不到的代码也会打到最后的bundle文件中,这就无形中增加了bundle的size。 Dead code elimination(简称DCE)是一种可以消除最后编译产物中无用的代码的技术,就以上出现的问题DCE技术可以移除无用的不相关的代码,以达到减小产物size的作用。

    function add(a, b) {
        const c = a + b;
        return c;
        const m = a * b;  // unreachable code
        return m;
    }
    
    function bar(a, b) {
        const c = a * b;
        if(0) {  // unreachable code
            // do something
        }
    }

    以上的代码就是一个典型的可以通过DCE消除掉的例子,但是在前端webpack中可以消除掉这些永远跑不到的代码,但是存在一个问题就是如果你import了一个模块,这个模块暴露出了两个函数,你在运行时只使用了其中一个,那么在最后打包的时候另外一个函数也会被打进去,代码如下:

    // helper.js
    export function foo() {
        return 'foo';
    }
    export function bar() {
        return 'bar';
    }
    
    // main.js
    import {foo} from './helpers';
    console.log(foo());

    如上在这种情况下,我们只使用了foo函数,没有使用bar函数,最后使用webpack打包之后,我们不需要bar函数的代码,因为没人去调用它,但是webpack如果没有配置tree shaking的话,bar还是会被打进最后的bundletree shaking就是一种DCE技术,发源于Lisp,最早在JS中是在google closure中使用,后来rollup的作者在rollup中引入了tree shaking这个技术,同时在webpack4中也实现了相应的功能。

    Tree Shaking的限制

    Tree Shaking的原理简单来说,就是程序的执行能够被一颗由函数调用组成的树表示,如果一个函数没有被调用到,那么这个函数就可以被消除掉;但是在commonJS中,module的加载不是静态的,这时候去做静态语法分析是比较困难的,但是es6 moudle的出现,使得这个问题变简单了,所以tree shaking的先决条件是你的module使用的是es6import/export

    Tree Shaking在Webpack中的使用

    配置webpack babel loader的插件:

     {
             loader: 'babel-loader',
             test: dir_js,
             query: {
             // presets: ['es2015'],
    
            // All of the plugins of babel-preset-es2015,
            // minus babel-plugin-transform-es2015-modules-commonjs
            plugins: [
              // 'babel-plugin-transform-es2015-modules-commonjs',
              'transform-es2015-template-literals',
              'transform-es2015-literals',
              'transform-es2015-function-name',
              'transform-es2015-arrow-functions',
              'transform-es2015-block-scoped-functions',
              'transform-es2015-classes',
              'transform-es2015-object-super',
              'transform-es2015-shorthand-properties',
              'transform-es2015-computed-properties',
              'transform-es2015-for-of',
              'transform-es2015-sticky-regex',
              'transform-es2015-unicode-regex',
              'check-es2015-constants',
              'transform-es2015-spread',
              'transform-es2015-parameters',
              'transform-es2015-destructuring',
              'transform-es2015-block-scoping',
              'transform-es2015-typeof-symbol',
              ['transform-regenerator', { async: false, asyncGenerators: false }],
       		],
                },
            }

    如果要让tree shaking起作用,那么将es6插件中的babel-plugin-transform-es2015-modules-commonjs去掉,避免打成commonJS的包,不然tree shaking没用。

    如下我们有以下代码,我们使用webpack来编译它们,来看看使用tree shaking和不使用的区别:

    	// helper.js
    	export function foo() {
    	if(0) {
          console.log('unreachable code');
        }
    	return 'foo';
    }
    export function bar() {
    	return 'bar';
    }
    
    // main.js
    import {foo} from './helpers';
    let elem = document.getElementById('output');
    console.log('test');
    console.log(foo)
    elem.innerHTML = `Output: ${foo()}`;

    使用webpack编译后的部分代码:

    tree shaking webpack0

    如上图是在没有启用tree-shaking的时候打包之后的bundle,这里在最后的bundle文件中有没有使用的bar函数,下图是使用了tree shaking之后打包后的代码:

    tree shaking webpack1

    可以看到在没有tree shaking的情况下没有使用的bar函数的代码也被打进了最后的bundle中,使用了tree shaking的将没有使用到的函数消除掉了。

Tree Shaking真的很美好吗?

看到这里你可能觉得tree shaking真的很美好,能够帮你把一切在运行时没有使用到的代码消除掉,但是遗憾的是,在实际情况下并没有这么美好,看看下面的例子:

	// helper.js
  // top level function
  export function foo() {
  	if(0) {
      	console.log('unreachable code');    
  	}
  	return 'foo';
	}
	export function bar() {
  	return 'bar';
	}
	
  // top level object
	export const MyObject = {
  	fooFuncInObj: () => {
      	return 'foo in obj';
  	},
  	barFuncInObj: () => {
      	return 'bar in obj';
  	},
	};
	
  
	export const MyObject1 = {
  	fooFuncInObj: () => {
      	return 'foo in obj';
  	},
  	barFuncInObj: () => {
      	return 'bar in obj';
  	},    
	};
	
  // top level variable
	export const fooVar = "I'm top level variable foo";
  export const barVar = "I'm top level variable bar";
	export const varObj = {
  	foo: 'variable foo in varObj',
      barVar: "variable bar in varObj",
	};
import { foo, MyObject, fooVar, varObj } from './helpers';
let elem = document.getElementById('output');
elem.innerHTML = `Output0: ${foo()} \n Output1: ${MyObject.fooFuncInObj()} \n Output2: ${fooVar}\n Output1: ${varObj.foo}`;

如果tree shaking足够聪明,那么我们这里仅仅调用了MyObject下的一个函数,另外一个函数没有使用,同样的我们只使用了varObj下的一个变量,另外的变量不应该打到最后的bundle里面去,遗憾的是现实情况并不是这样,下图是webpack打出来的包:

tree shaking webpack2

从上面的代码我们可以有这样的结论,webpacktree shaking对顶层函数、顶层变量和顶层对象有作用。

Reference


兴趣遍地都是,坚持和持之以恒才是稀缺的


Written by@wang yao
I explain with words and code.

GitHub