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

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

Open
youngwind opened this Issue Apr 20, 2016 · 24 comments

Comments

Projects
None yet
10 participants
@youngwind
Owner

youngwind commented Apr 20, 2016

问题

最近在做一个项目,用的是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的工具,貌似也是为了解决这个问题的,有空再研究研究。
@monkindey

This comment has been minimized.

monkindey commented May 26, 2016

写得真棒,
webpack2 已经有实现静态分析出没用的代码相应的功能,不过只是beta版。
rollup还是很多东西没法跟webpack比的,比如生态,比如hmr热替换等。
没法胜任项目应用层开发,用来打包js代码应该不错,vue作者就是用它来打包vuejs代码的。

@beiciye

This comment has been minimized.

beiciye commented Jun 20, 2016

babel-polyfill怎么抽出来的呢,如果我不把babel-polyfill打包进去,打包的后的页面会报错

@youngwind

This comment has been minimized.

Owner

youngwind commented Jun 20, 2016

直接不要在代码中require('babel-polyfill'),然后在html文件中直接引用polyfill(注意,polyfill必须在你写的js前引用) 。至于为什么可以这样做,你可以看看官网的说明。https://babeljs.io/docs/usage/polyfill/ @beiciye

@codelegant

This comment has been minimized.

codelegant commented Jul 21, 2016

朋友你好,我使用

  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

@youngwind

This comment has been minimized.

Owner

youngwind commented Jul 22, 2016

@codelegant “ReactDOM is not defined"这个错误是哪一行代码报的错,截图我看看。

@youngwind

This comment has been minimized.

Owner

youngwind commented Jul 22, 2016

@codelegant 还有你在html引用了那些react相关的js文件?我看你的邮件你是只引用了react-with-addon.js?

@codelegant

This comment has been minimized.

codelegant commented Jul 22, 2016

错误截图:
20160722130237

页面上只引用了 react-with-addon

webpack配置
编译后的脚本文件
页面文件

@youngwind

This comment has been minimized.

Owner

youngwind commented Jul 22, 2016

@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。

@codelegant

This comment has been minimized.

codelegant commented Jul 22, 2016

倘若我使用了 react-addons-css-transition-group react-addons-update等组件,分离之后,页面上该引用何种脚本才能便其工作正常?

@youngwind

This comment has been minimized.

Owner

youngwind commented Jul 22, 2016

@codelegant react-with-addons.js里面已经包含许多react-addon(插件),所以你只要引用react-with-addon.js就可以使用这些插件了。包含的插件列表可以参考这里。 https://facebook.github.io/react/docs/addons.html

@codelegant

This comment has been minimized.

codelegant commented Jul 22, 2016

那意思是要一个使用了 add-ons 插件的 react 应用,需要在页面中引入 react react-dom react-with-addons 三个包?

@youngwind

This comment has been minimized.

Owner

youngwind commented Jul 22, 2016

@codelegant 不是! 只需要引react-dom和react-with-addons,不需要再引react,因为react-with-addons就是带插件版本的react,react.js是不带插件版本的react.

@codelegant

This comment has been minimized.

codelegant commented Jul 23, 2016

多谢,我还以为 react-with-addons 是包括 react-dom 的,受教了。

@codelegant

This comment has been minimized.

codelegant commented Jul 24, 2016

再请教一个问题,我使用 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

@youngwind

This comment has been minimized.

Owner

youngwind commented Jul 24, 2016

@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里面额外配置什么。

@codelegant

This comment has been minimized.

codelegant commented Aug 4, 2016

使用 var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;这种方法的确可以引用外部文件,但如果使用 web-dev-server ,HMR ,react-hot-loader 进行开发调试,外引文件是无法热插拔的。必须使用 npm 包的方式,但写法就得修改成 import ReactCSSTransitionGroup from 'react-addons-css-transition-group',其中是否有我未知道的方式?可否两者统一?

@youngwind

This comment has been minimized.

Owner

youngwind commented Aug 4, 2016

@codelegant 我并不使用HMR,因为我对这个东西还没完全理解。我是采用结合webpack自带的watch和browsersync来实现热加载的功能的。另外,即便你适用web-dev-server,HMR这些东西,我并不认为你就能适用 'import ReactCSSTransitionGroup from 'react-addons-css-transition-group'写法。因为你在项目开发过程中,React.addon.js本身是不会发生改变的,自然无需watch它。

@codelegant

This comment has been minimized.

codelegant commented Aug 4, 2016

刚才试验,用 react 外引的方式 HMR 工作正常,只不过 react-hot-loader 没法用。以后坑还会很多,时不时的会叨扰兄台,还望勿怪。

@WhatAKitty

This comment has been minimized.

WhatAKitty commented Aug 7, 2016

@youngwind 你好,想请教下如果我使用预编译版本的react-with-addons.js文件,他的插件列表中并没有ReactInputSelection和ReactMount这两项,这该怎么处理?
如果是NPM INSTALL的话,可以直接通过require('react/lib/ReactMount')require('react/lib/ReactInputSelection')来获得

@renaesop

This comment has been minimized.

renaesop commented Sep 9, 2016

库里面的依赖,可以标记成peerdependencies吧

@youngwind youngwind added the 工程化 label Sep 29, 2016

@ian4hu

This comment has been minimized.

ian4hu commented Nov 1, 2016

贴下我的优化方式,部分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

@flyer5200

This comment has been minimized.

flyer5200 commented Dec 23, 2016

antd库怎么抽出来呢? 我的项目里面用了antd库, 我单独引用antd.js, 用externals 配置了antd, 打包完发现不能运行

@de8ug

This comment has been minimized.

de8ug commented Apr 1, 2017

good!bundle小了1M,多谢!

@ywzhaiqi

This comment has been minimized.

ywzhaiqi commented Jun 30, 2017

尝试了 externals,虽然大小变小了,构建速度变快了,但是手机加载变得极慢(特别是用了 antd、recharts 的 CDN)。还是要按需加载,减少手机解析 js 的时间(缓存变的不重要了)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment