webpack打包bundle.js体积大小优化

@youngwind 2016-04-20 00:11:54发表于 youngwind/blog webpack工程化

问题

最近在做一个项目,用的是react+redux+webpack,但是发现写着写着build出来的bundle.js(压缩前)居然已经有2.3M左右!开玩笑!我自己写的src目录底下的文件总大小也不过100多K,这也太夸张了吧。。。于是开始寻找优化的方法。

分析

先分析一下历史原因。
第一,在用webpack之前,做的项目都是jquery+后端渲染,一个页面请求巨多的js和css,导致性能问题。后来引入了react开发单页面应用的同时,使用webpack进行打包。所以,其实我们是很少经历说用webpack去打包jquery+后端渲染这样的项目的。
第二,由于开发的是单页面应用,不存在设置多个entry的做法,只能把js都build到一个bundle.js中(这里先不考虑根据router跳转按需请求的做法),所以最后build出来的唯一的bundle.js非常巨大,什么东西都往里面塞。

这样子导致的问题包括:

  1. 严重影响首次加载时间
  2. 每次有任何地方的修改,原先的缓存bundle.js都不能再使用,浪费带宽。

优化开始

其实当初用webpack是为了减少请求数,但是后来没能平衡好请求数和单个请求体积的问题。
如果能把里面常用的部分提取出来,放到cdn上缓存起来就好了
我觉得解决问题的第一步是:
分析巨大的bundle.js,看看里面都有啥,各个部分占据的体积是多少?

原始的供耕火种

先从webpack着手,

webpack --display-modules --sort-modules-by size

这个命令可以在打包的时候显示所有打包的模块以及他们的体积,并且按照体积从小到大进行排序。如图。
2016-04-20 8 28 38

我们翻到最后就能看到占据体积最大的module
2016-04-20 8 30 41

当然,这里显示的是我已经优化好的。

还原一下一开始场景:
我一开始在项目中引用了lodash,一个lodash400k啊!不仅如此,我还用了一个自己写的npm包,那个npm包也引用了lodash,关键是两个lodash依赖的版本还不一样。两个加起来就有900k了。。。(让我静一会儿。。。)这深深让我意识到前端的工具库可不能像后端那样随便引,要考虑体积啊!

然后接着分析,我只不过有了lodash很少一部分功能而已,没必要引用整个lodash包吧,所以又发现了lodash其实是有很多自己单独的包可以安装的。如图
2016-04-20 8 37 51

不错,用了单独安装的包之后体积减少了很多。但是我还是觉得减少的不够,所以我想使用is.js这玩意儿,但是死活没有搞定在webpack中打包出错的问题,见这儿

到这儿我心好累。。。心一横,不就几个判断和小工具嘛,最后我自己用原生的写了。。。所以就没有使用lodash和is.js。

工具范儿

ok,到这儿总算是“轻松加愉快”地解决了大头。然后,再分析剩余的1.3M左右的bundle.js。总不能一直这样用肉眼看上面终端输出的module列表吧,我知道肯定有人帮我们干了这事儿,坚持不懈的我找到了两个工具。

  1. https://github.com/webpack/analyse
  2. http://alexkuz.github.io/webpack-chart/

一开始用第一个工具的时候完全不会,我以为把bundle.js上传上去就好了,谁知道它要传什么json文件。(好歹你也给点提示啊。。。)等我找到第二个工具之后才发现需要生成一个json文件用于分析。

webpack --profile --json > stats.json

这两个工具做得实在太棒了!特别是第二个。
2016-04-20 8 47 29

有了工具干起活来就特别带劲!ok,现在不用看我都知道在剩余的1.3M当中占大头的肯定是react,压缩前600k呢!怎么把它从bundle.js搞出来呢?也是经历了一番波折。
最后我的解决方案:
第一,修改webpack.config.js

externals: {
    "react": 'React'
  },

第二,在html文件中单独引react.js

<script src={cdnPath}"/react.js"></script>

参考资料:

  1. http://webpack.github.io/docs/library-and-externals.html
  2. webpack/webpack#1275

目前为止,我们已经成功把react从bundle.js中提取出来,这样子我们就可以把react单独缓存起来了!我高高兴兴的重新分析bundle.js。
WTF!为什么里面还包含这么多react/lib目录下面的文件?加起来又是好几百k呢!如图。(没有截那种分析工具的图,就拿终端的将就着看吧。)
2016-04-20 8 59 13

又是react-css-transition-group

因为在项目中需要动画,用到了react-css-transition-group,react从v0.15版本开始就把addon从核心中剥离出来,具体的可以参考 #61 里面的安装部分。
我用第一个工具仔细分析了ReactDOMComponent.js的来源,一层一层地往上追溯,最后居然发现这个东西居然是因为react-css-transition-group引入的。。。我打开react-css-transition-group包看它的源码,发现只有一行。。。

module.exports = require('react/lib/ReactTransitionGroup');

其实这家伙又重新指回去了react库。我不就想用一个动画插件而已嘛。。至于付出几百k的代价吗?后来我一想,react虽然把addon从核心中移除了,但是react一直有一个带插件版本啊,我直接用带插件版本不就好了吗?
一对比,发现react-with-addons.js只比react.js大50k(压缩前),perfect!所以我又把react换成了带插件版本的,react-css-transition-group换一种引用方式。

// before
var ReactCSSTransitionGroup = require('react-addons-css-transition-group');

//after
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;

到这儿,文件大小已经控制在500k左右了。

babel-polyfill的坑

接着分析,发现babel-polyfill是个大头啊,200多k呢!我记得当初我引这个ployfill的时候是因为我在前端用到了co库,那时候引入了ployfill。其实我对babel-ployfill的了解很少,并不知道为什么一定要引入这个东西。以后有时间再研究。不过babel官网中提到这个东西可以单独引用,那就抽离吧!又可以多缓存200多k。

尾声

最后,我又用了抽离react一样的方法抽离了react-dom,react-router,history,redux,react-redux这几个常用的module,具体的webpack配置如下:

externals: {
    "react": 'React',
    "react-dom": "ReactDOM",
    "react-router": "ReactRouter",
    'history': "History",
    'redux': 'Redux',
    'react-redux': 'ReactRedux'
  },

最后将体积(压缩前)控制在170k,其中src代码占100k,成果还不错。

遗留问题

  1. 用工程化的手段保证react,redux等常用库缓存到cdn上,如果有必要,进行文件的拼接。
  2. 进行打包时可不可以主动分析用了哪些代码?然后只把用到的代码提取出来?听说webpack已经在做这方面的工作,而且我找了一个叫rollup的工具,貌似也是为了解决这个问题的,有空再研究研究。