blog
blog copied to clipboard
webpack打包bundle.js体积大小优化
问题
最近在做一个项目,用的是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非常巨大,什么东西都往里面塞。
这样子导致的问题包括:
- 严重影响首次加载时间
- 每次有任何地方的修改,原先的缓存bundle.js都不能再使用,浪费带宽。
优化开始
其实当初用webpack是为了减少请求数,但是后来没能平衡好请求数和单个请求体积的问题。 如果能把里面常用的部分提取出来,放到cdn上缓存起来就好了 我觉得解决问题的第一步是: 分析巨大的bundle.js,看看里面都有啥,各个部分占据的体积是多少?
原始的供耕火种
先从webpack着手,
webpack --display-modules --sort-modules-by size
这个命令可以在打包的时候显示所有打包的模块以及他们的体积,并且按照体积从小到大进行排序。如图。
我们翻到最后就能看到占据体积最大的module
当然,这里显示的是我已经优化好的。
还原一下一开始场景: 我一开始在项目中引用了lodash,一个lodash400k啊!不仅如此,我还用了一个自己写的npm包,那个npm包也引用了lodash,关键是两个lodash依赖的版本还不一样。两个加起来就有900k了。。。(让我静一会儿。。。)这深深让我意识到前端的工具库可不能像后端那样随便引,要考虑体积啊!
然后接着分析,我只不过有了lodash很少一部分功能而已,没必要引用整个lodash包吧,所以又发现了lodash其实是有很多自己单独的包可以安装的。如图
不错,用了单独安装的包之后体积减少了很多。但是我还是觉得减少的不够,所以我想使用is.js这玩意儿,但是死活没有搞定在webpack中打包出错的问题,见这儿。
到这儿我心好累。。。心一横,不就几个判断和小工具嘛,最后我自己用原生的写了。。。所以就没有使用lodash和is.js。
工具范儿
ok,到这儿总算是“轻松加愉快”地解决了大头。然后,再分析剩余的1.3M左右的bundle.js。总不能一直这样用肉眼看上面终端输出的module列表吧,我知道肯定有人帮我们干了这事儿,坚持不懈的我找到了两个工具。
- https://github.com/webpack/analyse
- http://alexkuz.github.io/webpack-chart/
一开始用第一个工具的时候完全不会,我以为把bundle.js上传上去就好了,谁知道它要传什么json文件。(好歹你也给点提示啊。。。)等我找到第二个工具之后才发现需要生成一个json文件用于分析。
webpack --profile --json > stats.json
这两个工具做得实在太棒了!特别是第二个。
有了工具干起活来就特别带劲!ok,现在不用看我都知道在剩余的1.3M当中占大头的肯定是react,压缩前600k呢!怎么把它从bundle.js搞出来呢?也是经历了一番波折。 最后我的解决方案: 第一,修改webpack.config.js
externals: {
"react": 'React'
},
第二,在html文件中单独引react.js
<script src={cdnPath}"/react.js"></script>
参考资料:
- http://webpack.github.io/docs/library-and-externals.html
- https://github.com/webpack/webpack/issues/1275
目前为止,我们已经成功把react从bundle.js中提取出来,这样子我们就可以把react单独缓存起来了!我高高兴兴的重新分析bundle.js。
WTF!为什么里面还包含这么多react/lib目录下面的文件?加起来又是好几百k呢!如图。(没有截那种分析工具的图,就拿终端的将就着看吧。)
又是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,成果还不错。
遗留问题
- 用工程化的手段保证react,redux等常用库缓存到cdn上,如果有必要,进行文件的拼接。
- 进行打包时可不可以主动分析用了哪些代码?然后只把用到的代码提取出来?听说webpack已经在做这方面的工作,而且我找了一个叫rollup的工具,貌似也是为了解决这个问题的,有空再研究研究。
写得真棒, webpack2 已经有实现静态分析出没用的代码相应的功能,不过只是beta版。 rollup还是很多东西没法跟webpack比的,比如生态,比如hmr热替换等。 没法胜任项目应用层开发,用来打包js代码应该不错,vue作者就是用它来打包vuejs代码的。
babel-polyfill怎么抽出来的呢,如果我不把babel-polyfill打包进去,打包的后的页面会报错
直接不要在代码中require('babel-polyfill'),然后在html文件中直接引用polyfill(注意,polyfill必须在你写的js前引用) 。至于为什么可以这样做,你可以看看官网的说明。https://babeljs.io/docs/usage/polyfill/ @beiciye
朋友你好,我使用
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
的方式将 react 包 排除,但是只能在页面使用 react 与 react-dom 的文件包,无法使用 react-with-addons 包。提示 Uncaught ReferenceError: ReactDOM is not defined
。
如果朋友你有什么解决方案,还望不吝赐教,十分感谢。
我的 webpack 配置:https://github.com/codelegant/react-action/blob/master/webpack.deploy.config.js
@codelegant “ReactDOM is not defined"这个错误是哪一行代码报的错,截图我看看。
@codelegant 还有你在html引用了那些react相关的js文件?我看你的邮件你是只引用了react-with-addon.js?
@codelegant 你这样只引用react-with-addon.js是不对了。因为react从版本v15开始,就把react-dom相关的部分抽离出核心,放在react-dom上面了,具体的你可以参考react在github上的release note,或者这里。 所以,你需要在html中单独引用React-DOM.js
另外,第97行代码的意思其实是将你引入的React-DOM.js重新包裹输出到webpack自己的模块系统中,模块的id为3。
倘若我使用了 react-addons-css-transition-group
react-addons-update
等组件,分离之后,页面上该引用何种脚本才能便其工作正常?
@codelegant react-with-addons.js里面已经包含许多react-addon(插件),所以你只要引用react-with-addon.js就可以使用这些插件了。包含的插件列表可以参考这里。 https://facebook.github.io/react/docs/addons.html
那意思是要一个使用了 add-ons 插件的 react 应用,需要在页面中引入 react react-dom react-with-addons 三个包?
@codelegant 不是! 只需要引react-dom和react-with-addons,不需要再引react,因为react-with-addons就是带插件版本的react,react.js是不带插件版本的react.
多谢,我还以为 react-with-addons 是包括 react-dom 的,受教了。
再请教一个问题,我使用 ES6 的方式引入插件:
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
webpack 中该如何配置才能使用打包后的文件能够在页面上使用 react-with-addons ?我的配置:
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
'react-addons-css-transition-group':'ReactCSSTransitionGroup'
}
然后页面出错:Uncaught ReferenceError: ReactCSSTransitionGroup is not defined
@codelegant 你这样写是有问题的,如果你直接require(你import也一个样)react-addons-css-transition-group,由于react-addons-css-transition-group会重新require react lib文件夹下的很多东西,所以你这样做是没法将react分离的。正确的做法上面已经提到了,你在引入了react-with-addons.js之后。
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
这样就可以拿到ReactCSSTransitionGroup了。 并不需要在webpack里面额外配置什么。
使用 var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
这种方法的确可以引用外部文件,但如果使用 web-dev-server ,HMR ,react-hot-loader 进行开发调试,外引文件是无法热插拔的。必须使用 npm 包的方式,但写法就得修改成 import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
,其中是否有我未知道的方式?可否两者统一?
@codelegant 我并不使用HMR,因为我对这个东西还没完全理解。我是采用结合webpack自带的watch和browsersync来实现热加载的功能的。另外,即便你适用web-dev-server,HMR这些东西,我并不认为你就能适用 'import ReactCSSTransitionGroup from 'react-addons-css-transition-group'写法。因为你在项目开发过程中,React.addon.js本身是不会发生改变的,自然无需watch它。
刚才试验,用 react 外引的方式 HMR 工作正常,只不过 react-hot-loader 没法用。以后坑还会很多,时不时的会叨扰兄台,还望勿怪。
@youngwind 你好,想请教下如果我使用预编译版本的react-with-addons.js文件,他的插件列表中并没有ReactInputSelection和ReactMount这两项,这该怎么处理?
如果是NPM INSTALL的话,可以直接通过require('react/lib/ReactMount')
和require('react/lib/ReactInputSelection')
来获得
库里面的依赖,可以标记成peerdependencies吧
贴下我的优化方式,部分polyfill和shim是为了兼容IE(这种预编译版本兼容至IE9,如果要兼容IE9以下,需要自行编译),如果不需要的就不用加了
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
'redux': 'Redux',
'redux-thunk': 'ReduxThunk',
'react-redux': 'ReactRedux',
'react-addons-css-transition-group': 'React.addons.CSSTransitionGroup',
'react-router': 'ReactRouter',
'react-router-redux': 'ReactRouterRedux',
'react-bootstrap': 'ReactBootstrap',
'babel-polyfill': 'window', // polyfill 直接写 {} 也是可以的
'es5-shim': 'window',
'whatwg-fetch': 'fetch',
'node-uuid': 'uuid',
'console-polyfill': 'console'
},
<script src="https://cdn.bootcss.com/es5-shim/4.5.9/es5-shim.min.js"></script>
<script src="https://cdn.bootcss.com/babel-polyfill/6.16.0/polyfill.min.js"></script>
<script src="https://cdn.bootcss.com/react/15.3.2/react-with-addons.min.js"></script>
<script src="https://cdn.bootcss.com/react/15.3.2/react-dom.min.js"></script>
<script src="https://cdn.bootcss.com/redux/3.6.0/redux.min.js"></script>
<script src="https://cdn.bootcss.com/react-redux/4.4.5/react-redux.min.js"></script>
<script src="https://cdn.bootcss.com/react-router/3.0.0/ReactRouter.min.js"></script>
<script src="https://cdn.bootcss.com/react-router-redux/4.0.6/ReactRouterRedux.min.js"></script>
<script src="https://cdn.bootcss.com/react-bootstrap/0.30.6/react-bootstrap.min.js"></script>
<script src="https://cdn.bootcss.com/redux-thunk/2.1.0/redux-thunk.min.js"></script>
<script src="https://cdn.bootcss.com/fetch/1.0.0/fetch.min.js"></script>
<script src="https://cdn.bootcss.com/node-uuid/1.4.7/uuid.min.js"></script>
<script src="https://cdn.bootcss.com/console-polyfill/0.2.3/index.min.js"></script>
经过上述精简后 bundle从800多K减少到160多K
如果有源码级调试或者其他需求,可以写两个webpack配置,调试构建使用不带externals,生产构建使用externals
antd库怎么抽出来呢? 我的项目里面用了antd库, 我单独引用antd.js, 用externals 配置了antd, 打包完发现不能运行
good!bundle小了1M,多谢!
尝试了 externals,虽然大小变小了,构建速度变快了,但是手机加载变得极慢(特别是用了 antd、recharts 的 CDN)。还是要按需加载,减少手机解析 js 的时间(缓存变的不重要了)。
想问下,这样子的话,如何使用import React from 'react'
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
想问下,这样子的话,如何使用import React from 'react'
externals: { 'react': 'React', 'react-dom': 'ReactDOM' }
要把script标签引入的React的js放在bundle之前引入,否则就会出现未定义\
已解决,感谢。
Ian Hu [email protected] 于2019年8月12日周一 上午9:00写道:
想问下,这样子的话,如何使用import React from 'react'
externals: {
'react': 'React', 'react-dom': 'ReactDOM'
}
[image: image] https://user-images.githubusercontent.com/16217324/62758043-cc3b6b00-baaf-11e9-8ca5-1e62a5a6b28d.png
要把script标签引入的React的js放在bundle之前引入,否则就会出现未定义\
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/youngwind/blog/issues/65?email_source=notifications&email_token=AD3XJ3CQ63357EOA6TBSQS3QECY2PA5CNFSM4CBM6MK2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD4BMTCY#issuecomment-520276363, or mute the thread https://github.com/notifications/unsubscribe-auth/AD3XJ3B5TFIZ3LPQKTFOKCTQECY2PANCNFSM4CBM6MKQ .
请问一下,nuxt框架上的webpack优化 使用cdn方式有研究过吗?