Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpacck之Tree shaking打包性能优化 #11

Open
liujie2019 opened this issue Feb 21, 2019 · 0 comments
Open

webpacck之Tree shaking打包性能优化 #11

liujie2019 opened this issue Feb 21, 2019 · 0 comments

Comments

@liujie2019
Copy link
Owner

liujie2019 commented Feb 21, 2019

1. 认识Tree

什么是Tree Shaking?字面意思是摇树,一句话:项目中没有使用的代码会在打包时候丢掉。JSTree Shaking依赖的是ES2015的模块系统(比如:import和export)。

Tree Shaking可以用来剔除javascript中用不上的死代码。依赖静态的ES6模块化语法,例如通过import和export导入、导出。Tree Shaking最先在Rollup中出现,Webpack2.0版本中将其引入。

为了更直观的理解它,来看一个具体的例子。假如有一个文件util.js里存放了很多工具函数和常量,在main.js中会导入和使用util.js,代码如下:

util.js源码:

export function funcA() {
}

export function funcB() {
}

main.js源码:

import { funcA } from './util.js';
funcA();

Tree Shaking后的util.js

export function funcA() {
}

由于只用到了util.js中的funcA,所以剩下的都被Tree Shaking当作死代码给剔除了。

需要注意的是:要让Tree Shaking正常工作的前提是交给WebpackJavaScript代码必须是采用ES6模块化语法的,因为ES6模块化语法是静态的(导入导出语句中的路径必须是静态的字符串,而且不能放入其它代码块中),这让Webpack可以简单的分析出哪些export的被import过了。如果采用ES5中的模块化,例如module.export = {...}、require(x+y)、if(x){require('./util')}Webpack无法分析出哪些代码可以剔除。

2. 接入Tree Shaking

上面讲了Tree Shaking是做什么的,接下来一步步教你如何配置WebpackTree Shaking生效。

首先,为了把采用ES6模块化的代码交给Webpack,需要配置Babel让其保留ES6模块化语句,修改.babelrc文件为如下:

{
  "presets": [
    [
      "env",
      {
        "modules": false
      }
    ]
  ]
}

其中"modules": false的含义是关闭Babel的模块转换功能,保留原本的 ES6模块化语法。

配置好Babel后,重新运行Webpack,在启动Webpack时带上--display-used-exports参数,以方便追踪Tree Shaking的工作, 这时你会发现在控制台中输出了如下的日志:

> webpack --display-used-exports
bundle.js  3.5 kB       0  [emitted]  main
   [0] ./main.js 41 bytes {0} [built]
   [1] ./util.js 511 bytes {0} [built]
       [only some exports used: funcA]

其中[only some exports used: funcA]提示了util.js只导出了用到的funcA,说明Webpack确实正确的分析出了如何剔除死代码。

但当你打开Webpack输出的bundle.js文件看下时,你会发现用不上的代码还在里面,如下:

/* harmony export (immutable) */
__webpack_exports__["a"] = funcA;

/* unused harmony export funB */

function funcA() {
  console.log('funcA');
}

function funB() {
  console.log('funcB');
}

Webpack只是指出了哪些函数用上了哪些没用上,要剔除用不上的代码还得经过 UglifyJS去处理一遍。要接入UglifyJS也很简单,不仅可以通过UglifyJSPlugin插件去实现,也可以简单的通过在启动Webpack时带上--optimize-minimize参数,为了快速验证Tree Shaking,我们采用较简单的后者来实验下。

通过webpack --display-used-exports --optimize-minimize重启 Webpack后,打开新输出的bundle.js,内容如下:

function r() {
  console.log("funcA")
}

t.a = r

可以看出Tree Shaking确实做到了,用不上的代码都被剔除了。

当我们的项目使用了大量第三方库时,你会发现Tree Shaking似乎不生效了,原因是大部分Npm中的代码都是采用的CommonJS语法,这导致Tree Shaking无法正常工作而降级处理。但幸运的时有些库考虑到了这点,这些库在发布到Npm上时会同时提供两份代码,一份采用CommonJS模块化语法,一份采用 ES6模块化语法。并且在package.json文件中分别指出这两份代码的入口。

redux库为例,其发布到 Npm 上的目录结构为:

node_modules/redux
|-- es
|   |-- index.js # 采用 ES6 模块化语法
|-- lib
|   |-- index.js # 采用 ES5 模块化语法
|-- package.json

package.json文件中有两个字段:

{
  "main": "lib/index.js", // 指明采用 CommonJS 模块化的代码入口
  "jsnext:main": "es/index.js" // 指明采用 ES6 模块化的代码入口
}

mainFields用于配置采用哪个字段作为模块的入口描述。为了让Tree Shakingredux生效,需要配置Webpack的文件寻找规则为如下:

module.exports = {
  resolve: {
    // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
    mainFields: ['jsnext:main', 'browser', 'main']
  },
};

以上配置的含义是优先使用jsnext:main作为入口,如果不存在jsnext:main就采用browser或者main作为入口。虽然并不是每个Npm 中的第三方模块都会提供ES6模块化语法的代码,但对于提供了的不能放过,能优化的就优化。

目前越来越多的Npm中的第三方模块考虑到了Tree Shaking,并对其提供了支持。采用jsnext:main作为ES6模块化代码的入口是社区的一个约定,假如将来你要发布一个库到Npm时,希望你能支持Tree Shaking,以让Tree Shaking发挥更大的优化效果,让更多的人为此受益。
image

3. webpack4中使用Tree Shaking

需要注意的是:在webpack4中使用Tree Shaking,不再需要[uglifyjs-webpack-plugin]
image
image
image

参考博文

  1. webpack 如何通过作用域分析消除无用代码
  2. Tree-Shaking性能优化实践 - 原理篇
  3. webpack4 系列教程(八): JS Tree Shaking
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

No branches or pull requests

1 participant