美文网首页前端面试
webpack打包优化

webpack打包优化

作者: 艾晨星雨 | 来源:发表于2018-09-16 18:07 被阅读1920次

    记一次react项目优化的过程
    优化前,用uglifyjs-webpack-plugin插件压缩js后得到的大小,实际大小1.3M,如图:

    image.png
    以上图形化界面,可下载webpack-bundle-analyzer插件包查看。
    可以发现打包出来的main.js内主要包含了两部分:node_modules包及js业务代码,所以第一步就是要拆分node_modules包及业务代码:
    //业务代码与node_modules分离
    new webpack.optimize.CommonsChunkPlugin({    
              name: 'vendor',
              minChunks: ({ resource }) => (
                  resource &&
                  resource.indexOf('node_modules') >= 0 &&
                  resource.match(/\.js$/)
              ),
          }),
    

    打包效果:


    image2.png

    第二步:分离node_modules包中比较大的包,并通过CDN或以静态文件的方式引入到项目中,在webpack中用externals指定即可,如:

    externals: {
            'react': 'React',
            'react-dom': 'ReactDOM',
            'redux': 'Redux',
            'redux-thunk': 'ReduxThunk',
            'react-redux': 'ReactRedux',
            'redux-form': 'ReduxForm',
            'immutable': 'Immutable',
            'babel-polyfill': 'window', // polyfill 直接写 {} 也是可以的,
            'transit-js': 'transit',
        }
    

    分离后在externals中所指定的包就不会打包的vendor.js中。
    1.在index.html中通过CDN的方式引入在externals中指定过的包,如:

      //注:以下插件包需找项目中对应版本的包且是min.js压缩版的
      <script src="https://cdn.bootcss.com/babel-polyfill/6.2.0/polyfill.min.js"></script>
      <script src="https://cdn.bootcss.com/react/16.4.0/umd/react.production.min.js"></script>
      <script src="https://cdn.bootcss.com/react-dom/16.4.0/umd/react-dom.production.min.js"></script>
      <script src="http://cdn.cognitect.com/transit/transit-0.8.861-min.js"></script>
      <script src="https://cdn.bootcss.com/redux-thunk/2.2.0/redux-thunk.min.js"></script>
      <script src="https://cdn.bootcss.com/redux/3.7.2/redux.min.js"></script>
      <script src="https://cdn.bootcss.com/react-redux/5.0.7/react-redux.min.js"></script>
      <script src="https://cdn.bootcss.com/redux-form/7.0.1/redux-form.min.js"></script>
      <script src="https://cdn.bootcss.com/immutable/3.8.1/immutable.min.js"></script>
    

    什么是CDN?
    cdn(内容分发网络)的作用是加速网络传输,通过将资源部署到服务器,来加快资源到获取速度。目前用到的CDN主要是由Bootstrap 中文网支持并维护的前端开源项目免费 CDN 服务。

    2.以静态文件的方式引入资源包
    在当前项目的public目录下创建新的目录resource,将以上cdn引用包全都保存到resource目录(注:通过create-react-app创建的react项目中public目录下默认会有static目录,这里不能将js到静态文件放到static目录,在index.html中直接引入static目录下到文件会报错 Unexpected token <,所以要么将static目录名改成其他名字,或者新建一个目录)

    image3.jpg
      <script src="/resource/polyfill.min.js"></script>
      <script src="/resource/react.production.min.js"></script>
      <script src="/resource/react-dom.production.min.js"></script>
      <script src="/resource/transit-0.8.861-min.js"></script>
      <script src="/resource/redux-thunk.min.js"></script>
      <script src="/resource/redux.min.js"></script>
      <script src="/resource/react-redux.min.js"></script>
      <script src="/resource/redux-form.min.js"></script>
      <script src="/resource/immutable.min.js"></script>
    

    既然用CDN加速来加载引入的包,为什么还要用静态文件的方式引入呢?
    答案当然是本地引入文件会更快,毕竟cdn也是服务环境,请求服务环境的资源也是耗时间的,可参考cdn加速与js引入文件的比较
    分离后vendor.js的大小:从原来的1.06M减少到584.42kb,小了一半

    image4@2x.png
    注:细心的同学会发现,上图中的antd-mobile包为什么没有通过js引用?
    原因是antd-mobile包在经过按需打包后所得大小是147kb,而通过cdn引入的antd-mobile竟然有373kb,远超出了打包所得大小,所以不建议通过js引入,
    可参考cdn antd-mobile

    3.第三步-拆分js业务代码,按需加载
    可看上图image2,右侧蓝色部分,业务代码实际大小只有93kb,但在页面初次加载资源时会将整个js业务代码都加载进来,特别是在网络环境差时,会加载的比较慢
    业务代码主要分三个模块:买家账户(放account目录),卖家(放seller目录),登录、产品等页面(放sys目录),再加上公共代码(common\component目录),总共分四个块,项目中用到react-router-dom路由,接下来就按路由的方式拆分代码,所用工具react-loadable

    //目录结构
    Projects
      --js
        --common
        --component
        --App.web.js
        --pages
          --index.web.js
          --account
          --sys
          --seller
    index.web.js
    
    //App.web.js
    import {
        AsyncSys,
        AsyncSeller,
        AsyncAccount,
    } from './pages/index.web'
    import {Route, Redirect, Switch } from 'react-router-dom'
    class Root extends Component {
        render() {
            return (
                <div>
                    <Switch>
                        <Route exact path="/" render={() => (
                            <Redirect to="/Sys/Product"/>
                        )}/>
                        <Route path="/Sys" component={AsyncSys}/>
                        <Route path="/Account" component={AsyncAccount}/>
                        <Route path="/SellerList" component={AsyncSeller}/>
                    </Switch>
                </div>
            )
        }
    }
    
    //./pages/index.web.js 这里按路由拆分
    import Loadable from 'react-loadable';
    import AsyncJSLoading from '../component/AsyncJSLoading'
    
    const AsyncSys = Loadable({
        loader: () => import(/* webpackChunkName: 'sys' */'./sys/index.web'),
        loading: AsyncJSLoading
    });
    
    const AsyncSeller = Loadable({
        loader: () => import(/* webpackChunkName: 'seller' */'./seller/index.web'),
        loading: AsyncJSLoading
    });
    
    const AsyncAccount = Loadable({
        loader: () => import(/* webpackChunkName: 'account' */'./account/index.web'),
        loading: AsyncJSLoading
    });
    export {
        AsyncSys,
        AsyncAccount,
        AsyncSeller,
    }
    
    //./sys/index.web.js
    import {Route, Switch} from 'react-router-dom';
    import Product from './ProductDetails.web';
    import Login from './Login.web';
    import SupplyData from './SupplyData.web';
    
    export default class extends React.Component {
        render() {
            return (
                <div>
                    <Switch location={this.props.location}>
                        <Route path={`${this.props.match.path}/Login`} component={Login}/>
                        <Route path={`${this.props.match.path}/Product`} component={Product}/>
                        <Route path={`${this.props.match.path}/SupplyData`} component={SupplyData}/>
                    </Switch>
                </div>
            )
        }
    }
    //./seller/index.web.js 、./account/index.web.js 与 sys/index.web.js写法一样,这里不列举了
    

    3点注意:
    1.在 pages/index.web.js中通过import分割相应模块,模块内的文件在当前分割js中不可再次引用,否则分割失效,如:

    //./pages/index.web.js 
    import Loadable from 'react-loadable';
    import AsyncJSLoading from '../component/AsyncJSLoading'
    import Login from './sys/Login.web'    //再次引入sys目录下的文件
    
    const AsyncSys = Loadable({
        loader: () => import(/* webpackChunkName: 'sys' */'./sys/index.web'),
        loading: AsyncJSLoading
    });
    
    

    2.import() 会返回一个Promise,对于不支持Promise浏览器需要在页面上注入Promise polyfill,如:

    <script src="https://cdn.bootcss.com/babel-polyfill/6.2.0/polyfill.min.js"></script>
    

    另外import()语法还没有加入到ECMAScript标准里,所以项目中需要安装一个Babel插件 babel-plugin-syntax-dynamic-import,并且将其加入.babelrc中:

    {
    plugins: [
        "syntax-dynamic-import"
     ],
    }
    

    3./* webpackChunkName: 'sys' */ 的含义是为动态生成的Chunk赋予一个名称,以便我们追踪和调试代码

    //filename中的name 就是 webpackChunkName所指定的名称
    output: {
        path: paths.appBuild,
        filename: 'resource/js/[name].[chunkhash:8].js',
        chunkFilename: 'resource/js/[name].[chunkhash:8].chunk.js',
        publicPath: publicPath,
      },
    

    最后看看分割的结果:按预期分成功了四大块:main.js 主要是公共代码、account.js 是买家模块、seller.js 是卖家模块、sys.js 是 登录、产品页部分


    image5.png

    第四步-gzip压缩
    什么是gzip压缩?
    参考
    服务器开启GZIP
    探索HTTP传输中gzip压缩的秘密

    const CompressionWebpackPlugin = require('compression-webpack-plugin');
    
    webpackConfig.plugins.push(
        new CompressionWebpackPlugin({
          asset: '[path].gz[query]',
          algorithm: 'gzip',
          test: new RegExp('\\.(js|css)$'),
          threshold: 10240,
          minRatio: 0.8
        })
    )
    

    压缩前:最大的文件vendor.js 589kb

    image6.png
    压缩后:vendor.js 154kb
    image7.png

    至此,大功告成!

    相关文章

      网友评论

        本文标题:webpack打包优化

        本文链接:https://www.haomeiwen.com/subject/kqztnftx.html