使用 webpack 内置的 stats

stats:构建的统计信息。

  1. "script": {
  2. "build:stats": "webpack --config webpack.prod.js --json > stats.json"
  3. // ...
  4. }

速度分析:speed-measure-webpack-plugin

可以看到每个 loader 和插件执行耗时。

速度分析插件作用

分析整个打包总耗时。

分析每个插件和 loader 的耗时情况。

配置

npm i speed-measure-webpack-plugin -D
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');

const smp = new SpeedMeasureWebpackPlugin();

module.exports = smp.wrap({
  // ...
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano')
    }),
    new CleanWebpackPlugin(),
    // new FriendlyErrorsWebpaclPlugin(),
    // function () {
    //   this.hooks.done.tap('done', (stats) => {
    //     if (
    //         stats.compilation.errors && 
    //         stats.compilation.errors.length && 
    //         process.argv.indexOf('--watch') == -1
    //       ) {
    //       console.log('build error');
    //       process.exit(1);
    //     }
    //   })
    // }
  ].concat(htmlWebpackPlugins)
})

打印信息如下。

  module count = 13
raw-loader, and
babel-loader, and
thread-loader, and
babel-loader took 0.922 secs
  module count = 1
css-loader, and
less-loader, and
postcss-loader, and
px2rem-loader took 0.734 secs
  module count = 2
raw-loader took 0.183 secs
  module count = 1
url-loader took 0.022 secs
  module count = 1
html-webpack-plugin took 0.017 secs
  module count = 2
modules with no loaders took 0.011 secs
  module count = 1

体积分析:使用 webpack-bundle-analyzer

构建完成后会在 8888 端口展示大小。

可以分析哪些问题

依赖的第三方模块大小。

业务里面的组件代码大小。

配置

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

module.exports = {
  // ...
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano')
    }),
    new CleanWebpackPlugin(),
    new FriendlyErrorsWebpaclPlugin(),
    function () {
      this.hooks.done.tap('done', (stats) => {
        if (
            stats.compilation.errors && 
            stats.compilation.errors.length && 
            process.argv.indexOf('--watch') == -1
          ) {
          console.log('build error');
          process.exit(1);
        }
      })
    },
    new BundleAnalyzerPlugin()
  ].concat(htmlWebpackPlugins)
}

亲测好用。

使用高版本的 webpack 和 nodejs

webpack4 内部优化

  • V8 带来的优化(for of 替代 forEach、Map 和 Set 替代 Object、includes 替代 indexOf)。
  • 默认使用更快的 md4 hash 算法。
  • webpacks AST 可以直接从 loader 传递给 AST,减少解析时间。
  • 使用字符串方法替代正则表达式。

多进程多实例构建

可选方案

  • thread-loader
  • parallel-webpack
  • HappyPack

HappyPack 解析资源

原理:每次 webpack 解析一个模块,HappyPack 会将它及它的依赖分配给 worker 线程中。

webpack4 原生提供 thread-loader 模块,HappyPack 已不再维护。

npm i happypack -D

thread-loader 解析资源

原理:每次 webpack 解析一个模块,thread-loader 会将它及它的依赖分配给 worker 线程中。

npm i thread-loader -D
const path = require('path');
const glob = require('glob');

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: 3
            }
          },
          'babel-loader',
          // 'eslint-loader'
        ]
      }
  },
 // ...
}

经测试,靠谱。

多进程多实例并行压缩

使用 parallel-uglify-plugin 插件

使用 uglifyjs-webpack-plugin 开启 parallel 参数

使用 terser-webpack-plugin 开启 parallel 参数

webpack4 推荐使用 terser-webpack-plugin。

npm i terser-webpack-plugin -D
const TerserWebpackPlugin = require('terser-webpack-plugin');

module.exports = {
  // ...
  optimization: {
    // ...
    minimizer: [
      new TerserWebpackPlugin({
        parallel: true
      })
    ]
  }
}

分包:预编译资源模块

设置 Externals

思路:将 react、react-dom 基础包通过 cdn 引入,不打入 bundle 中。

方法:使用 html-webpack-externals-plugin。

使用预编译资源模块

思路:将 react、react-dom、redux、react-redux 基础包和业务基础包打包成一个文件。

方法:使用 DLLPlugin 进行分包,DllReferencePlugin 对 mainfest.json 引用。

webpack.dll.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'production',
  entry: {
    library: [
      'react',
      'react-dom'
    ]
  },
  output: {
    filename: '[name]_[chunkhash].dll.js',
    path: path.join(__dirname, 'build/library'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_[hash]',
      path: path.join(__dirname, 'build/library/[name].json')
    })
  ]
}

package.json

"scripts": {
  "dll": "webpack --config webpack.dll.js"
}

wbpack.prod.js

const webpack = require('webpack');

module.exports = {
  // ...
  // optimization: {
  //   splitChunks: {
  //     minSize: 0,
  //     cacheGroups: {
  //       vendors: {
  //         test: /(react|react-dom)/,
  //         name: 'vendors',
  //         chunks: 'all',
  //         priority: -10
  //       },
  //       commons: {
  //         name: 'commons',
  //         chunks: 'all',
  //         minChunks: 2,
  //         priority: -20
  //       }
  //     }
  //   }
  stats: 'errors-only',
  plugins: [
    // ...
    // new BundleAnalyzerPlugin()
    new webpack.DllReferencePlugin({
      manifest: require('./build/library/library.json')
    })
  ].concat(htmlWebpackPlugins)
}

利用缓存提升二次构建速度

缓存

目的:提升二次构建速度。

缓存思路:

  • babel-loader 开启缓存
  • terser-webpack-plugin 开启缓存
  • 使用 cache-loader 或者 hard-source-webpack-plugin

babel-loader 缓存

'babel-loader?cacheDirectory=true'

terser-webpack-plugin

新版本无 cache 属性。

hard-source-webpack-plugin

npm i hard-source-webpack-plugin -D

webpack 5 无效。

缩小构建目标

目的:尽可能的少构建模块。

比如 babel-loader 不解析 node_modules。

减少文件搜索范围

优化 resolve.modules 配置(减少模块搜索层级)。

优化 resolve.mainFields 配置。

优化 resolve.extensions 配置。

合理使用 alias。

module.exports = {
  // resolve: {
  //   alias: {
  //     'react': path.resolve(__dirname, './node_modules/react/umd/react.production.min.js'),
  //     'react-dom': path.resolve(__dirname, './node_modules/react-dom/umd/react-dom.production.min.js')
  //   },
  //   extensions: ['.js'],
  //   mainFields: ['main']
  // },
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          {
            loader: 'thread-loader',
            options: {
              workers: 3
            }
          },
          'babel-loader',
          // 'babel-loader?cacheDirectory=true',
          // 'eslint-loader'
        ]
      }
    ]
  }
}

resolve 配置测试效果不好,include 效果还可以。

Tree Shaking 擦除无用的 JS 和 CSS

概念:1 个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到 bundle 里,tree shaking 就是只把用到的方法打入 bundle,没用到的方法则会在 uglify 阶段被擦除掉。

使用:webpack 默认支持,在 .babelrc 里设置 modules: false 即可。

.production mode 的情况下默认开启。

要求:必须是 ES6 的语法,CJS 的方式不支持。

无用的 CSS 如何删除掉?

PurifyCSS:遍历代码,识别已经用到的 CSS class。

uncss:HTML 需要通过 jsdom 加载,所有的样式通过 PostCSS 解析,通过 doucument.querySelector 来识别在 html 文件里面不存在的选择器。

如何使用 PurifyCSS

使用 purgecss-webpack-plugin 和 mini-css-extract-plugin 配合使用。

npm i purgecss-webpack-plugin -D
const PurgecssWebpackPlugin = require('purgecss-webpack-plugin');

const PATHS = {
  src: path.join(__dirname, 'src')
}

module.exports = {
  // ....
  plugins: [
    // ...
    new PurgecssWebpackPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true })
    })
  ].concat(htmlWebpackPlugins)
}

图片压缩

要求:基于 Node 库的 imagemin 或者 tinypng API。

使用:配置 image-webpack-loader。

Imagemin 的优点分析

有很多定制选项 。

可以引入更多第三方优化插件,例如 pngquant 。

可以处理多种图片格式。

Imagemin 的压缩原理

pngquant: 是一款 PNG 压缩器,通过将图像转换为具有 alpha 通道(通常比24/32位PNG 文件小60-80%)的更高效的 8 位 PNG 格式,可显著减小文件大小。

pngcrush: 其主要目的是通过尝试不同的压缩级别和 PNG 过滤方法来降低 PNG IDAT 数据流的大小。

optipng: 其设计灵感来自于pngcrush。optipng 可将图像文件重新压缩为更小尺寸,而不会丢失任何信息。

tinypng: 也是将 24 位 png 文件转化为更小有索引的 8 位图片,同时所有非必要的 metadata 也会被剥离掉。

配置

npm i image-webpack-loader -D
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              name: '[name]_[hash:8].[ext]',
              limit: 10240
            },
          },
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65
              },
              // optipng.enabled: false will disable optipng
              optipng: {
                enabled: false,
              },
              pngquant: {
                quality: [0.65, 0.90],
                speed: 4
              },
              gifsicle: {
                interlaced: false,
              },
              // the webp option will enable WEBP
              webp: {
                quality: 75
              }
            }
          }
        ]
      }
    ]
  }
}

使用动态 Polyfill

Promise 的浏览器支持情况

promise.png

动态 Polyfill 方案

polyfill.png

Polyfill Service 原理

识别 User Agent,下发不同的 Polyfill。

如何动态使用 Polyfill Service

polyfill.io 官方提供的服务 。

<script src="https://cdn.polyfill.io/v3/polyfill.min.js"></script>

基于官方自建 polyfill 服务 。

体积优化策略

Scope Hoisting 。

Tree-shaking 。

公共资源分离 。

图片压缩 。

动态 Polyfill。