webpack打包经验——处理打包文件体积过大的问题

前言

最近对一个比较老的公司项目作了一次优化,处理的主要是webpack打包文件体积过大的问题。css

这里就写一下对于webpack打包优化的一些经验。html

主要分为如下几个方面:node

  1. 去掉开发环境下的配置
  2. ExtractTextPlugin:提取样式到css文件
  3. webpack-bundle-analyzer:webpack打包文件体积和依赖关系的可视化
  4. CommonsChunkPlugin:提取通用模块文件
  5. 提取manifest:让提取的公共js的hash值不要改变
  6. 压缩js,css,图片
  7. react-router 4 以前的按需加载
  8. react-router 4 的按需加载
  9. react v16.6以后 的按需加载(2019.07.04更新)

本篇博客用到的webpack插件如何配置均可以去查看我写的这篇博客:react

【Webpack的使用指南 02】Webpack的经常使用解决方案webpack

这里就不细讲这些配置了。ios

去掉开发环境下的配置

好比webpack中的devtool改成false,不须要热加载这类只用于开发环境的东西。git

这些不算是优化,而算是错误了。github

对于在开发环境下才有用的东西在打包到生产环境时统统去掉。web

ExtractTextPlugin:提取样式到css文件

将样式提取到单独的css文件,而不是内嵌到打包的js文件中。npm

这样带来的好处时分离出来的css和js是能够并行下载的,这样能够更快地加载样式和脚本。

解决方案:

安装ExtractTextPlugin

npm i --save-dev extract-text-webpack-plugin

而后修改webpack.config.js为:

const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    // ...
    new ExtractTextPlugin({ filename: '[name].[contenthash].css', allChunks: false }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        exclude: /node_modules/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: ['css-loader?modules', 'postcss-loader'],
        }),
      }, {
        test: /\.css$/,
        include: /node_modules/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: ['css-loader', 'postcss-loader'],
        }),
      },
      {
        test: /\.less$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: ['css-loader?modules', 'less-loader', 'postcss-loader'],
        }),
      },
    ],
  },
}

打包后生成文件以下:

webpack-bundle-analyzer:webpack打包文件体积和依赖关系的可视化

这个东西不算是优化,而是让咱们能够清晰得看到各个包的输出文件体积与交互关系。

安装:

npm install --save-dev webpack-bundle-analyzer

而后修改webpack.config.js:

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = merge(common, {
  // ...
  plugins: [
    new BundleAnalyzerPlugin({ analyzerPort: 8919 })
  ],
});

打包后会自动出现一个端口为8919的站点,站点内容以下:

能够看到咱们打包后的main.js中的代码一部分来自node_modules文件夹中的模块,一部分来自本身写的代码,也就是src文件夹中的代码。

为了以后描述方便,这个图咱们直接翻译过来就叫webpack打包分析图。

CommonsChunkPlugin:提取通用模块文件

所谓通用模块,就是如react,react-dom,redux,axios几乎每一个页面都会应用到的js模块。

将这些js模块提取出来放到一个文件中,不只能够缩小主文件的大小,在第一次下载的时候能并行下载,提升加载效率,更重要的是这些文件的代码几乎不会变更,那么每次打包发布后,仍然会沿用缓存,从而提升了加载效率。

而对于那些多文件入口的应用更是有效,由于在加载不一样的页面时,这部分代码是公共的,直接能够从缓存中应用。

这个东西不须要安装,直接修改webpack的配置文件便可:

const webpack = require('webpack');

module.exports = {
  entry: {
    main: ['babel-polyfill', './src/app.js'],
    vendor: [
      'react',
      'react-dom',
      'redux',
      'react-router-dom',
      'react-redux',
      'redux-actions',
      'axios'
    ]
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      names: ['vendor'],
      minChunks: Infinity,
      filename: 'common.bundle.[chunkhash].js',
    })
  ]
}

打包后的webpack打包分析图为:

能够很明显看到react这些模块都被打包进了common.js中。

提取manifest:让提取的公共js的hash值不要改变

当咱们了解webpack中的hash值时,通常都会看到[hash]和[chunkhash]两种hash值的配置。

其中hash根据每次编译的内容计算获得,因此每编译一次全部文件都会生成一个新的hash,也就彻底没法利用缓存。

因此咱们这里用了[chunkhash],chunkhash是根据内容来生成的,因此若是内容不改变,那么生成的hash值就不会改变。

chunkhash适用于通常的状况,可是,对于咱们以上的状况是不适用的。

我去改变主文件代码,而后生成的两个公共js代码的chunkhash值却改变了,它们并无使用到主文件。

因而我用文本对比工具,对比了它们的代码,发现只有一行代码是有差异的:

这是由于webpack在执行时会有一个带有模块标识的运行时代码。

当咱们不提取vendor包的时候这段代码会被打包到main.js文件中。

当咱们提取vendor到common.js时,这段脚本会被注入到common.js里面,而main.js中没有这段脚本了了.

当咱们将库文件分为两个包提取出来,分别为common1.js和common2.js,发现这段脚本只出如今一个common1.js中,而且
那段标识代码变成了:

u.src=t.p+""+e+"."+{0:"9237ad6420af10443d7f",1:"be5ff93ec752c5169d4c"}

而后发现其余包的首部都会有个这样的代码:

webpackJsonp([1],{2:functio

这个运行时脚本的代码正好和其余包开始的那段代码中的数字相对应。

咱们能够将这部分代码提取到一个单独的js中,这样打包的公共js就不会受到影响。

咱们能够进行以下配置:

plugins: [
   new webpack.optimize.CommonsChunkPlugin({
     names: ['vendor'],
     minChunks: Infinity,
     filename: 'common.bundle.[chunkhash].js',
   }),
   new webpack.optimize.CommonsChunkPlugin({
     names: ['manifest'],
     filename: 'manifest.bundle.[chunkhash].js',
   }),
   new webpack.HashedModuleIdsPlugin()
 ]

对于names而言,若是chunk已经在entry中定义了,那么就会根据entry中的入口提取chunk文件。若是没有定义,好比mainifest,那么就会生成一个空的chunk文件,来提取其余全部chunk的公共代码。

而咱们这段代码的意思就是将webpack注入到包中的那段公共代码提取出来。

打包后的文件:

webpack打包分析图:

看到图中绿色的那个块了吗?

那个东西就是打包后的manifest文件。

这样处理后,当咱们再修改主文件中的代码时,生成的公共js的chunkhash是不会改变的,改变的是那个单独提取出来的manifest.bundle.[chunkhash].js的chunkhash。

压缩js,css,图片

这个其实不许备记录进来,由于这些通常项目应该都具有了,不过这里仍是顺带提一句吧。

压缩js和css一步便可:

webpack -p

图片的压缩:

image-webpack-loader

具体的使用请查看 Webpack的经常使用解决方案 的第16点。

react-router 4 以前的按需加载

若是使用过Ant Design 通常都知道有一个配置按需加载的功能,就是在最后打包的时候只把用到的组件代码打包。

而对于通常的react组件其实也有一个使用react-router实现按需加载的玩法。

对于每个路由而言,其余路由的代码实际上并非必须的,因此当切换到某一个路由后,若是只加载这个路由的代码,那么首屏加载的速度将大大提高。

首先在webpack的output中配置

output: {
  // ...
  chunkFilename: '[name].[chunkhash:5].chunk.js',
},

而后须要将react-router的加载改成按需加载,例如对于下面这样的代码:

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route } from 'react-router-dom';
import PageMain from './components/pageMain';
import PageSearch from './components/pageSearch';
import PageReader from './components/pageReader';
import reducer from './reducers';

const store = createStore(reducer);
const App = () => (
  <Provider store={store}>
    <Router>
      <div>
        <Route exact path="/" component={PageMain} />
        <Route path="/search" component={PageSearch} />
        <Route path="/reader/:bookid/:link" component={PageReader} />
      </div>
    </Router>
  </Provider>
);

应该改成:

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import { HashRouter as Router, Route } from 'react-router-dom';
import reducer from './reducers';

const store = createStore(reducer);

const PageMain = (location, callback) => {
  require.ensure([], require => {
    callback(null, require('./components/pageMain').default);
  }, 'PageMain');
};

const PageSearch = (location, callback) => {
  require.ensure([], require => {
    callback(null, require('./components/pageSearch').default);
  }, 'PageSearch');
};

const PageReader = (location, callback) => {
  require.ensure([], require => {
    callback(null, require('./components/pageReader').default);
  }, 'PageReader');
};

const App = () => (
  <Provider store={store}>
    <Router>
      <div>
        <Route exact path="/" getComponent={PageMain} />
        <Route path="/search" getComponent={PageSearch} />
        <Route path="/reader/:bookid/:link" getComponent={PageReader} />
      </div>
    </Router>
  </Provider>
);

react-router 4 的按需加载

上面那种方法应用到react-router 4上是行不通的,由于getComponent方法已经被移除了。

而后我参考了官方教程的方法

在这里咱们须要用到webpack, babel-plugin-syntax-dynamic-import和 react-loadable。

webpack内建了动态加载,可是咱们由于用到了babel,因此须要去用babel-plugin-syntax-dynamic-import避免作一些额外的转换。

因此首先须要

npm i babel-plugin-syntax-dynamic-import  --save-dev

而后在.babelrc加入配置:

"plugins": [
  "syntax-dynamic-import"
]

接下来咱们须要用到react-loadable,它是一个用于动态加载组件的高阶组件。
这是官网上的一个例子

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}

使用起来并不难,Loadable函数会传入一个参数对象,返回一个渲染到界面上的组件。
这个参数对象的loader属性就是须要动态加载的组件,而loading这个属性传入的是一个展现加载状态的组件,当尚未加载出动态组件时,展现在界面上的就是这个loading组件。

使用这种方法相对于原来的方式优点很明显,咱们不仅是在路由上能够进行动态加载了,咱们动态加载的组件粒度能够更细,好比一个时钟组件,而不是像以前那样每每是一个页面。

经过灵活去使用动态加载能够完美控制加载的js的大小,从而使首屏加载时间和其余页面加载时间控制到一个相对平衡的度。

这里有个点须要注意,就是一般咱们在使用loading组件时常常会出现的问题:闪烁现象。

这种现象的缘由是,在加载真正的组件前,会出现loading页面,可是组件加载很快,就会致使loading页面出现的时间很短,从而形成闪烁。

解决的方法就是加个属性delay

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
  delay: 200
});

只有当加载时间大于200ms时loading组件才会出现。

还有更多的关于react-loadable的玩法:https://www.npmjs.com/package/react-loadable

那么如今看下咱们的打包文件:

webpack打包分析图:

注意看看上面的打包文件名字,发现经过这种方法进行按需加载的几个文件都是按照数字命名,而没有按照咱们指望的组件名命名。

我在这个项目的github上面找了一下,发现它提供的按组件命名的方法须要用到服务端渲染,而后就没有继续下去了。

反正这个东西也不是很重要,因此就没有进一步深究,若是有园友对这个问题有好的办法,也但愿能在评论里说明。

react v16.6以后 的按需加载(2019.07.04更新)

React这个版本新加了lazy和Suspense这两个功能。

对于上面的按需加载,能够修改代码为:

import React, { Suspense } from 'react';
import Loading from './my-loading-component';

const LoadableComponent = React.lazy(() => import('./my-component'));

export default class App extends React.Component {
  render() {
    return  (
       <Suspense fallback={<Loading />}>
         <LoadableComponent/>;
       </Suspense>
    )
  }
}

临时更新,写得简单点,见谅!

总结

总的来说,经过以上步骤应该是能够解决绝大多数打包文件体积过大的问题。

固然,由于文中webpack版本和插件版本的差别,在配置和玩法上会有一些不一样,可是上面描述的这些方向都是没有问题的,而且相信在各个版本下均可以找到相应的解决方案。

文中若有疑误,请不吝赐教。

相关文章
相关标签/搜索