webpack之优化篇(一)

优化静态资源

图片资源

对于项目中的图片资源,我们主要从两方面进行优化:

  1. 对资源进行压缩,减少体积。
  2. 减少请求的数量,降低服务器压力。

对资源进行压缩

减少体积,我们使用对应的loader进行处理。使用file-loader来处理图片文件

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.*\.(gif|png|jpe?g|svg|webp)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {}
          },
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: { // 压缩 jpeg 的配置
                progressive: true,
                quality: 65
              },
              optipng: { // 使用 imagemin-optipng 压缩 png,enable: false 为关闭
                enabled: false,
              },
              pngquant: { // 使用 imagemin-pngquant 压缩 png
                quality: '65-90',
                speed: 4
              },
              gifsicle: { // 压缩 gif 的配置
                interlaced: false,
              },
              webp: { // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
                quality: 75
              },
            }
          }
        ],
      },
    ],
  },
}
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

将一些图片转换为base64 uri

减少http请求时,就可以用url-loader进行处理

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        exclude: [resolve('src')],
        options: {
          limit: 10000, // 限制文件的最大大小
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

CSS Sprites

当我们的项目中,使用了大量的svg-icon时,可以用svg-sprite-loader进行处理

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: 'svg-sprite-loader',
        // 只对相应目录下的资源做处理
        include: [resolve('src/icons')],
        options: {}
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

html模版

html-webpack-plugin将index.html与打包后的进行关联,其实,html-webpack-plugin中还有提供输出配置minify:

当mode为production时,minify配置生效

new HtmlWebpackPlugin({
  filename: 'index.html',
  template: 'index.html',
  inject: true,
  title: 'admin',
  minify: {
    // 合并空格
    collapseWhitespace: true,
    // 移除注解
    removeComments: true,
    // 移除多余的属性
    removeRedundantAttributes: true,
    // 移除脚本类型属性
    removeScriptTypeAttributes: true,
    // 移除样式类型属性
    removeStyleLinkTypeAttributes: true,
    // 使用简短的文档类型
    useShortDoctype: true
    // more options:
    // https://github.com/kangax/html-minifier#options-quick-reference
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

CSS剥离与压缩

extract-text-webpack-plugin(还不支持webpack的4.x版本)和mini-css-extract-plugin是可以提取CSS到单独的文件中。


 module: {
    rules: [
      {
        test: /\.css/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader // 涉及到内容装转换
          },
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    // extract css into its own file
    // Error contenthash not implemented with webpack > 4.3.0
    // 1. yarn upgrade extract-text-webpack-plugin@next
    // 2. 采用 mini-css-extract-plugin
    new MiniCssExtractPlugin({
      // 因为webpack4.3包含了contenthash这个关键字,所以ExtractTextPlugin中不能使用contenthash
      // 使用md5:contenthash:hex:8代替contenthash
      // github issue https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/765
      filename: 'css/[name].[contenthash].css',
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      allChunks: true,
    })
  ]

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

JavaScript

webpack4.x中已经可以将压缩的插件放置在minimizer中了。用UglifyJsPlugin进行JS代码压缩

module.exports = {
  mode: 'production',
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        uglifyOptions: {
          mangle: {
            safari10: true
          },
          // 清除生产环境的控制台输出
          compress: {
            warnings: false,
            drop_debugger: true,
            drop_console: true
          }  
        },
        sourceMap: config.build.productionSourceMap,
        cache: true,
        parallel: true
      }),
      // Compress extracted CSS. We are using this plugin so that possible
      // duplicated CSS from different components can be deduped.
      new OptimizeCSSAssetsPlugin()
    ]
  }
}
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

关于代码分离

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。 代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,果使用合理,会极大影响加载时间。

常用的代码分离方法有三种:

  1. 入口起点:使用 entry 配置手动地分离代码。
  2. 防止重复:使用 SplitChunksPlugin 去重和分离 chunk。
  3. 动态导入:通过模块中的内联函数调用来分离代码。

入口起点

我们可以通过配置多个入口文件,从而进行代码分离

module.exports = {
 context: path.resolve(__dirname, '../'),
 entry: {
   app: ['./src/main.js', './src/index.js']
 },
 output: {
   path: config.build.assetsRoot,
   filename: '[name].js'
 }
}
1
2
3
4
5
6
7
8
9
10

但是,这样做有两个缺点:

如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。 这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。

防止重复

optimization中的splitChunks即可做到

注意:splitChunks是webpack4.x的解决方案,webpack3.x是用CommonsChunkPlugin插件

webpack3.x

 new webpack.optimize.CommonsChunkPlugin(options)
1

webpack4.x

 const path = require('path');

  module.exports = {
    mode: 'development',
    entry: {
      index: './src/index.js',
      another: './src/another-module.js',
    },
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
    optimization: {
      splitChunks: {
        chunks: 'all', // chunks 代码公共的部分分离出来成为一个单独的文件
      }
    }
  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

更详细的配置内容:

optimization: {
    minimize: env === 'production' ? true : false, // 开发环境不压缩
    splitChunks: {
        chunks: "async", // 共有三个值可选:initial(初始模块)、async(按需加载模块)和all(全部模块)
        minSize: 30000, // 模块超过30k自动被抽离成公共模块
        minChunks: 1, // 模块被引用>=1次,便分割
        maxAsyncRequests: 5,  // 异步加载chunk的并发请求数量<=5
        maxInitialRequests: 3, // 一个入口并发加载的chunk数量<=3
        name: true, // 默认由模块名+hash命名,名称相同时多个模块将合并为1个,可以设置为function
        automaticNameDelimiter: '~', // 命名分隔符
        cacheGroups: { // 缓存组,会继承和覆盖splitChunks的配置
            default: { // 模块缓存规则,设置为false,默认缓存组将禁用
                minChunks: 2, // 模块被引用>=2次,拆分至vendors公共模块
                priority: -20, // 优先级
                reuseExistingChunk: true, // 默认使用已有的模块
            },
            vendors: {
                test: /[\\/]node_modules[\\/]/, // 表示默认拆分node_modules中的模块
                priority: -10
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

splitChunks是拆包优化的重点,如果你的项目中包含 element-ui 等第三方组件(组件较大),建议单独拆包,如下所示:

splitChunks: {
    // ...
    cacheGroups: {    
        elementUI: {
            name: "chunk-elementUI", // 单独将 elementUI 拆包
            priority: 10, // 权重需大于其它缓存组
            test: /[\/]node_modules[\/]element-ui[\/]/
        }
    }
}
1
2
3
4
5
6
7
8
9
10

动态导入

使用ECMAScript的 import(), webpack 会自动处理使用该语法编写的模块。

// import 作为一个方法使用,传入模块名即可,返回一个 promise 来获取模块暴露的对象
// 注释 webpackChunkName: "lodash" 指定 chunk 的名称,输出文件时有用
import(/* webpackChunkName: "lodash" */ 'lodash').then((_) => { 
  console.log(_.lash([1, 2, 3])) // 打印 3
})
1
2
3
4
5