目录

Webpack打包优化

目录

之前了解了webpack的基本打包流程或者说原理,如果只是配置一个基本的webpack打包配置, 打包后的文件会变得很大,当你项目部署后,用户打开对应的界面,也许会很长时间才加载完(当然和网络环境或硬件设备也有关系),尤其是单页面应用效果很明显,这样的用户体验自然是不好的,所以此文章主要是对webpack打包优化的一个小小总结

首先搭建好一个基本的项目,目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
├── dist // 打包目录
   ├── bundle.js // 打包后的js
   └── index.html // 入口html文件
├── package-lock.json
├── package.json
├── public
   └── index.html // html模板文件
├── src
   ├── actions // redux actions文件
      └── index.js
   ├── app.js // webpack打包入口文件,也是项目的入口文件
   ├── assets // scss
      ├── about.scss
      ├── title.scss
      └── user.scss
   ├── dev.js // 开发时的一些配置
   ├── pages // page页面
      ├── about.jsx
      ├── index.jsx
      ├── title.jsx
      └── user.jsx
   ├── reducers // redux中的reducer
      └── index.js
   └── store.js
└── webpack.config.js

webpack基本配置如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const path = require('path')
const HtmlWebPackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/app.js',
  output: {
    path: path.resolve(__dirname, './dist/'),
    filename: 'bundle.js'
  },
  resolve: {
    extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx']
  },

  devServer: {
    contentBase: path.join(__dirname, './src/'),
    publicPath: '/',
    host: '127.0.0.1',
    port: 3000,
    hot: true,
    stats: {
      colors: true
    }
  },
  module: {
    rules: [{
      test: /\.jsx?$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader'
      }
    }, {
      test: /\.scss$/,
      use: ['style-loader', 'css-loader', 'sass-loader']
    }]
  },
  plugins: [
    new HtmlWebPackPlugin({
      template: 'public/index.html',
      filename: 'index.html',
      inject: true
    })
  ]
}

优化:

1.基础优化

配置loader,添加excludeinclude缩小搜索范围,比如

1
2
3
4
5
6
7
{
  test: /\.jsx?$/,
    exclude: /node_modules/, // 过滤node_modules
    use: {
    	loader: 'babel-loader'
    }
}

2.使用DllPlugin

将一些变动较少或者根本不会变动的库先打包生成对应的.dll.js,在webpack打包的时候,将这些资源引入

首先新建一个webpack.config.dll.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');

module.exports = {
  entry: {
    react: ['react', 'react-dom']
  },
  output: {
    filename: '[name].dll.js',
    // 生成的文件目录
    path: path.resolve(__dirname, 'dist'),
    // 动态链接库名称
    library: '_dll_[name]'
  },
  plugins: [
    new DllPlugin({
      name: '_dll_[name]',
      path: path.join(__dirname, 'dist', '[name].manifest.json'),
      context: __dirname, 
    })
  ]
}

然后在package.jsonscripts中添加dll打包命令

1
webpack --config webpack.config.dll.js

然后运行此命令,可以看到会在dist目录下面生成react.dllreact.manifest.json两个文件,这就是生成的dll文件,然后webpack.config.js里面添加配置,打包时引入这些的dll文件。

首先安装插件add-asset-html-webpack-plugin,引入DllReferencePlugin

1
2
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')

插件里面配置(webpack4+)

1
2
3
4
5
6
7
new DllReferencePlugin({
  context: __dirname,
  manifest: require('./dist/react.manifest.json'),
}),
new AddAssetHtmlPlugin({
  filepath: path.resolve(__dirname, './dist/react.dll.js')
})

这样打包时页面就会引入react.dll.js资源了,从而减少重复的资源打包。这种方式可以简单的理解为,抽出公共模块打包,然后引入

3.splitChunks

以前webpack拆分模块还会用CommonsChunkPlugin,在webpack4后,可以直接用splitChunks来代替完成这项工作,在optimization里面设置了splitChunks后,打包分割出来的文件是默认压缩过的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
splitChunks: {
  chunks: "async", // "initial" | "all" | "async",对哪种代码进行分割
  minSize: 30000, // 超过minSize的包才做代码分割
  minChunks: 1, // 一个包至少被用了多少次的时候才进行代码分割
  maxAsyncRequests: 5, // 按需加载最多能加载多少个模块
  maxInitialRequests: 3, // 对于entry里面的文件做代码分割最多能生成多少个js文件
  automaticNameDelimiter: '~', // 文件生成时的连接符
  name: true, // 为true的时候,打包出来的文件名由cacheGroups里面设置的为准
  cacheGroups: {
    vendors: {
      test: /[\\/]node_modules[\\/]/, // 匹配哪些需要分割的模块
      priority: -10, // 优先级
     	filename: 'vendors.js'// 打包到一个叫vendors.js的文件
    },
    default: {
      minChunks: 2,
      priority: -20,
      reuseExistingChunk: true
    }
  }
}

4.catch-loader

在一些性能开销较大的loader之前添加此 loader,以将结果缓存到磁盘里,不做过多介绍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: [
          'cache-loader',
          ...loaders
        ],
        include: path.resolve('src')
      }
    ]
  }
}

5.HappyPack

HappyPack使用node多线程进行构建来提升构建的速度,使用情况较少

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({
  size: os.cpus().length
})

......
plugins: [
  new HtmlWebPackPlugin({
    template: 'public/index.html',
    filename: 'index.html',
    inject: true
  }),
  new HappyPack({
    id: 'happyBabel',
    loaders: [{
      loader: 'babel-loader?cacheDirectory=true',
    }],
    threadPool: happyThreadPool,
    verbose: true,
  })
],

6.css的压缩

对于scss文件,一般来讲,依次配置sass-loadercss-loaderstyle-loader来进行处理

1
2
3
4
{
  test: /\.scss$/,
  use: ['style-loader', 'css-loader', 'sass-loader']
}

最后加载完后的页面css会以内联样式的形式签入到页面中。在webpack4中使用mini-css-extract-plugin插件来提取、压缩css。首先将style-loadeMiniCssExtractPlugin.loader代替,然后使用MiniCssExtractPlugin插件,打包出对应的文件,并在页面中引入对应的css文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

......

{
  test: /\.scss$/,
  use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
}

new MiniCssExtractPlugin({
  filename: '[name].[hash:5].css',
  chunkFilename: '[id].[hash:5].css'
})

7.图片的处理

同样的,先用file-loader解析图片保证webpack能处理,然后可以用image-webpack-loader压缩图片,或者说用url-loader将图片转为base64编码的形式,不多赘述

8.CDN加速 通常在打包中,把不变的一些静态文件放到CDN上,可以直观地减小资源包大小,比如项目中用到了clipboard.js,然后在模板html中引入这个文件

1
<script src="https://cdn.bootcdn.net/ajax/libs/clipboard.js/2.0.6/clipboard.min.js" defer></script>

一般来讲,这种方式引入js静态文件,那么全局对象(如window)里面是会有对应的对象的,就可以直接引用对应的对象。当然还可以加上

1
2
3
externals: {
  clipboard: 'clipboard'
},

这样就保证了打包时不打包clipboard,优化打包后的文件体积

还有一种方式就是把所有的静态资源,如jscss都放在相应的CDN上,在打包的config中指定对应资源的CDN域名,同样配置externals。但其实这种方式有个很大的缺点就是,如果全部这样做的话,万一CDN挂了,整个页面也就挂了

9.按需加载

react为例,使用react-loadable来做按需加载,以前还有些其它方法这里就不赘述。主要是针对react-router做一个处理,首先安装依赖react-loadable,入口文件中封装一个loading组件,用于页面加载时给一个提示,然后封装一个方法,用于异步加载这些组件

1
2
3
4
5
6
7
8
import Loadable from 'react-loadable'
const Loading = (props) => {
  return <div>这是一个loading组件</div>
};
const asyncLoad = loader => Loadable({
  loader,
  loading: Loading
})

以下有三个页面,indexaboutuser,调用asyncLoad,引入这些文件

1
2
3
const Index = asyncLoad(() => import('./pages/index'))
const About = asyncLoad(() => import('./pages/about'))
const User = asyncLoad(() => import('./pages/user'))

再配置好路由,这样打包出来的js文件就只会在对应页面或者说对应路由命中时加载了,从而提升页面的一个加载速度,当然也把打包出来的js文件进一步的分割,减小体积,当然需要注意的是,要在webpack.config里面配置好chunFilename(按需加载的chunk名字),资源的publickPath

1
2
3
4
5
6
7
<Router>
  <Switch>
  	<Route path="/" exact component={Index} />
    <Route path="/about/" component={About} />
    <Route path="/users/" component={User} />
  </Switch>
</Router>

webpack.config.js

1
2
3
4
5
6
output: {
  path: path.resolve(__dirname, './dist/'),
  filename: '[name].[hash:5].bundle.js',
  chunkFilename: '[name].[hash:5].bundle.js',
  publicPath: '/'
},