youngwind / blog

梁少峰的个人博客

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

youngwind opened this issue · comments

问题

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

写得真棒,
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?

错误截图:
20160722130237

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

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

@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

commented

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

commented

good!bundle小了1M,多谢!

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

想问下,这样子的话,如何使用import React from 'react'

externals: {
       'react': 'React',
       'react-dom': 'ReactDOM'
  }

image

想问下,这样子的话,如何使用import React from 'react'

externals: {
       'react': 'React',
       'react-dom': 'ReactDOM'
  }

image

要把script标签引入的React的js放在bundle之前引入,否则就会出现未定义\

请问一下,nuxt框架上的webpack优化 使用cdn方式有研究过吗?