webpack5性能优化篇-前端必须会系列-分析如何加快构建并将包体积变小

1,969 阅读10分钟

前言

继上一话《将公司旧webpack3升级到5,踩坑总结》之后,我发现了几个问题。

  • 构建速度是否可以再快一点。
  • 构建的包的大小为什么比之前还大了一点,能否更小。 根据以上几点,我便寻找解决方案。找到实用的几个分享出来,一些踩过的坑都列出来了。期间将介绍如何分析最终可实现构建速度减少50%,包体积大小大幅度减小的效果。

构建速度部分

构建时间观察

首先分析构建速度,这里我使用speed-measure-webpack-plugin插件。这个插件可以查看各个plugins和Loaders过程的耗时。一般使用方法。

const webpackConfig = {
  ····
  plugins: [new MyPlugin(), new MyOtherPlugin()],
};

to:

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
  ···
  plugins: [new MyPlugin(), new MyOtherPlugin()],
});

但是笔者再使用speed-measure-webpack-plugin的时候出现了一些问题。会和mini-css-extract-plugin以及vue-loader不兼容出现的报错。

vue-loader was used without the corresponding plugin. Make sure to include VueLoaderPlugin in your webpack config······

Error: You forgot to add 'mini-css-extract-plugin' plugin····

找了很久找到一种能解决的方案,原帖。然后我改造一下webpack.prod.conf.js。

······
const miniCssExtractPluginConfig =  new MiniCssExtractPlugin({
  filename: utils.assetsPath('css/[name].css'),
  chunkFilename: utils.assetsPath('css/[id].[contenthash].css')
})
······
let webpackConfig = merge(baseWebpackConfig, {
    // 一些webpack的配置
    ······
    plugins: [
        ······
        miniCssExtractPluginConfig,
        ······
    ]
})
······
// 是否开启speed分析的开关
if (config.build.buildSpeedReport) {
  let index = webpackConfig.plugins.findIndex(plugin => {
    return plugin === miniCssExtractPluginConfig
  })
  webpackConfig.plugins.splice(index, 1)
  const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
  const smp = new SpeedMeasurePlugin()
  webpackConfig = smp.wrap(webpackConfig)
  webpackConfig.plugins.push(miniCssExtractPluginConfig)
  const { VueLoaderPlugin } = require('vue-loader')
  webpackConfig.plugins.push(new VueLoaderPlugin())
}
module.exports = webpackConfig

然后不出意外就会有这样的效果,最初的构建一共花了近1分钟。

 SMPGeneral output time took 57.3 secs

 SMPPlugins
TerserPlugin took 13.43 secs
CssMinimizerPlugin took 4.82 secs
BundleAnalyzerPlugin took 3.32 secs
CopyPlugin took 0.152 secs
CompressionPlugin took 0.14 secs
HtmlWebpackPlugin took 0.088 secs
VueLoaderPlugin took 0.046 secs
DefinePlugin took 0.028 secs
ProvidePlugin took 0.016 secs

 SMPLoaders
_mini-css-extract-plugin@2.6.0@mini-css-extract-plugin, and
_css-loader@6.7.1@css-loader, and
_postcss-loader@6.2.1@postcss-loader, and
_sass-loader@6.0.6@sass-loader, and
_sass-resources-loader@2.2.4@sass-resources-loader took 31.25 secs
  module count = 4
_css-loader@6.7.1@css-loader, and
_postcss-loader@6.2.1@postcss-loader, and
_sass-loader@6.0.6@sass-loader, and
_sass-resources-loader@2.2.4@sass-resources-loader took 30.71 secs
······

分析

可以看到在所有plugin里面,TerserPlugin花的时间最长。loader主要是css-loader、sass-loader和style-loader。

  • 优化TerserPlugin.terserMinify 可以看到官方的minify默认使用TerserPlugin.terserMinify可以使用 esbuild或者swc进行替换。但是这两种都有缺点。

esbuild:

⚠ the extractComments option is not supported and all legal comments (i.e. copyright, licenses and etc) will be preserved 读者使用esbuild进行替换。

swc:

⚠ the extractComments option is not supported and all comments will be removed by default, it will be fixed in future

也就是说目前使用esbuild进行代码压缩就会保留注释,而使用swc的话就没有注释。对这个有特殊要求的话读者可以自己斟酌或者使用默认的。笔者使用esbuild方式进行演示。因为swc方式我报了一个out of bounds错误目前还没有解决。读者有遇到可评论告诉我。 因为使用esbuild,所以需要先装对应依赖。不然会报Cannot find module 'esbuild' error。

npm install esbuild -D

然后再修改TerserPlugin对应配置。把minify换成TerserPlugin.esbuildMinify。

new TerserPlugin({
  minify: TerserPlugin.esbuildMinify
}),

然后build一下。

 SMPGeneral output time took 43.47 secs

 SMPPlugins
CssMinimizerPlugin took 4.75 secs
BundleAnalyzerPlugin took 3.42 secs
TerserPlugin took 1.094 secs
CopyPlugin took 0.139 secs
CompressionPlugin took 0.137 secs
HtmlWebpackPlugin took 0.088 secs
VueLoaderPlugin took 0.045 secs
DefinePlugin took 0.033 secs
ProvidePlugin took 0.026 secs

 SMPLoaders
······

TerserPlugin一下子缩短到了1秒。真奇迹。但是我看了一下包体积大小,比之前大了100-200KB,这个笔者自己斟酌是否在意(后续在包体优化篇用了gzip之后这个包体差距就更小了)。

  • 在每个loader里面增加范围限制include或者exclude,如:
{
    test: /.svg$/,
    loader: 'svg-sprite-loader',
    include: resolve('src/icons')
},
{
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    exclude: resolve('src/icons'),
    type: 'asset',
    parser: {
      dataUrlCondition: {
        maxSize: 8 * 1024,
      },
    },
    generator: {
      filename: utils.assetsPath('img/[name].[hash:7][ext]')
    }
}
······

实际效果:

 SMPGeneral output time took 43.2 secs

 SMPPlugins
······
 SMPLoaders
_mini-css-extract-plugin@2.6.0@mini-css-extract-plugin, and
_css-loader@6.7.1@css-loader, and
_postcss-loader@6.2.1@postcss-loader, and
_sass-loader@6.0.6@sass-loader, and
_sass-resources-loader@2.2.4@sass-resources-loader took 31.42 secs

一共耗时43.2差不了很多

  • babel-loader 开启缓存 在对应babel-loader loader的options打开cacheDirectory。官方介绍

cacheDirectory:默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程(recompilation process)。如果设置了一个空值 (loader: 'babel-loader?cacheDirectory') 或者 true (loader: 'babel-loader?cacheDirectory=true'),loader 将使用默认的缓存目录 node_modules/.cache/babel-loader,如果在任何根目录下都没有找到 node_modules 目录,将会降级回退到操作系统默认的临时文件目录。

{
  test: /.js$/,
  loader: 'babel-loader',
  include: resolve('src'),
  options: {
    cacheDirectory: true
  }
},

第一开启cacheDirectory并构建,一共使用46.68

 SMPGeneral output time took 46.68 secs

 SMPPlugins
·····
 SMPLoaders
_mini-css-extract-plugin@2.6.0@mini-css-extract-plugin, and
_css-loader@6.7.1@css-loader, and
_postcss-loader@6.2.1@postcss-loader, and
_sass-loader@6.0.6@sass-loader, and
_sass-resources-loader@2.2.4@sass-resources-loader took 34.87 secs

第二次构建:

 SMPGeneral output time took 37.37 secs
····
 SMPPlugins
····
 SMPLoaders
_mini-css-extract-plugin@2.6.0@mini-css-extract-plugin, and
_css-loader@6.7.1@css-loader, and
_postcss-loader@6.2.1@postcss-loader, and
_sass-loader@6.0.6@sass-loader, and
_sass-resources-loader@2.2.4@sass-resources-loader took 25.32 secs
····

花了37.37秒,比没开启前少了6秒左右。可以看到loaders花费减少的时间差不多就是总共耗时减少的时间。这都是babel-loader开启cacheDirectory的效果。

npm install cache-loader -D

在时间较久的loader里面增加cash-loader,官方demo:

module.exports = {
  module: {
    rules: [
      {
        test/.ext$/,
        use: ['cache-loader', ...loaders],
        include: path.resolve('src'),
      },
    ],
  },
};

读者是在在build=》utils.js, MiniCssExtractPlugin.loader后面加上,必须在MiniCssExtractPlugin.loader后面,不然会出现第二次构建没有css文件情况,详情见Doesn't work with mini-css-extract

if (options.extract) {
  return [{
    loader: MiniCssExtractPlugin.loader,
    options: {
      publicPath: '../../'
    }
  }, 'cache-loader'].concat(loaders)
} else {
  return ['style-loader'].concat(loaders)
}

加上cache-loader,第一次build一共耗时39.34秒:

 SMPGeneral output time took 39.34 secs
···
 SMPPlugins
····
 SMPLoaders
_mini-css-extract-plugin@2.6.0@mini-css-extract-plugin, and
_cache-loader@4.1.0@cache-loader, and
_css-loader@6.7.1@css-loader, and
_postcss-loader@6.2.1@postcss-loader, and
_sass-loader@6.0.6@sass-loader, and
_sass-resources-loader@2.2.4@sass-resources-loader took 27.84 secs

第二次有缓存之后,loader一共需要34.09秒了:

 SMPGeneral output time took 34.088 secs

 SMPPlugins
······
 SMPLoaders
_image-webpack-loader@8.1.0@image-webpack-loader took 18.76 secs
  module count = 398
modules with no loaders took 16.54 secs
  module count = 1890
_babel-loader@7.1.1@babel-loader took 10.46 secs

速度也能提升个4秒左右。

  • webpack5提供最新的缓存方式cache。官方介绍。 该方法据说可以大幅减少构建时间。经典配置:
cache: {
    type: "filesystem",
    buildDependencies: {
        config: [ __filename ] 
    }
}

但是笔者使用的时候出现了JAVA OOM ERROR,再把node内存限制改成10G后,第一次构建时间花了8分钟也没结束,node内存使用一度超过8G,索性取消,怀疑出现了内存泄漏或者哪里的bug,GOOGLE也查不到解决方案,只查到内存泄漏类似情况,读者若是有解决方案可以评论告诉我,我也好更新文章。

构建速度部分总结:

笔者使用了speed-measure-webpack-plugin进行查看分析构建时主要loader和plugins的耗时,并通过:

  1. TerserPlugin.terserMinify替换为esbuild方式(减少13s左右)
  2. include exclude限制构建目标(效果不明显,减少1s不到)
  3. babel-loader通过cacheDirectory增加缓存,减少第二次构建时间(减少6秒)。
  4. 通过cache-loader缓存,减少主要耗时的loader的时间(减少4秒)。
  5. 官方推荐cashe(笔者未使用) 以上五种方式,让构建时间间从最初的一分钟左右减少到目前34秒。突然感觉时间都多了起来。

缩小包体积部分

包体积观察

首先分析构建速度,这里我使用webpack-bundle-analyzer插件。官方介绍:

  1. Realize what's really inside your bundle(了解bundle中有哪些模块及其组成)
  2. Find out what modules make up the most of its size(找到模块里面体积最大的那个)
  3. Find modules that got there by mistake(找到因为失误而导入的模块)
  4. Optimize it!(优化)

安装:

npm i webpack-bundle-analyzer -D

使用:

// 是否开启bundleAnalyzerReport的开关
if (config.build.bundleAnalyzerReport) {
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin({ analyzerMode: 'static' }))
}

不出意外会有这个效果(gif图来自官方): 93f72404-b338-11e6-92d4-9a365550a701.gif 我的效果图(由于是公司项目结构,这边做了马赛克处理):

image.png

分析:

  1. vendors里面echarts,element以及xlsx,vue所占空间很大。这边想办法剥离出来。
  2. app.js里面的assets资源文件里面的图片占用空间很多,需要进行处理。 处理之前vendors.js为3.63MB,app.js为3.35MB,现在根据以上进行对应处理。
  • 使用webpack externals externals引入外部库, 无需webpack打包处理。并通过cdn方式引入。

这边将xlsx,element-ui,echarts进行剥离。因为element-ui需要vue先初始化否则会报Cannot read properties of undefined (reading 'prototype')错误。所以这边一并将vue全家桶也剥离出来。 externals配置:

module.exports = {
······
externals: {
    'xlsx': 'XLSX',
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'element-ui': 'ELEMENT',
    'echarts': 'echarts'
  }
}

index.html加入对应cdn链接:

<script src="xxxx/vue.min.js"></script>
<script src="xxxx/vue-router.min.js"></script>
<script src="xxxx/vuex.min.js"></script>
<script src="xxxx/xlsx.min.js"></script>
<script src="xxxx/element-2.13.2/index.js"></script>
<script src="xxxx/echarts-4.9.0/echarts.min.js"></script>

注意点:

  1. 剥离element-ui之后需要将Vue.use(ElementUI)这句话也去掉。
  2. 不可单独剥离ElementUI因为ElementUI导入前必须先有Vue(若读者发现可以不剥离Vue同时element-ui也顺利剥离的话请评论区留言,我进行调整)
  3. 如果使用了vue-echarts那么需要在externals加上'echarts/lib/echarts': 'echarts'
externals: {
······
    'echarts': 'echarts',
    'echarts/lib/echarts': 'echarts',
  }
}
  1. 剥离echarts之后需要将之前按需引入的,全去掉。不然还会有一部分打包到vendors.js里面。
// import 'echarts/lib/chart/bar'
// import 'echarts/lib/chart/radar'
·······

这样调整完之后vendors只有1.53MB了,比之前少了2MB。当然还可以继续将lodash,jquery拿出来。但是这些相对于以上几个modules就比较小了。是否要拿出来看笔者了。

  • 使用image-webpack-loader 我们观察到assets里面的图片所长空间还是很大的。image-webpack-loader可以将图片进行压缩。进而减小app.js内图片的空间。

安装:

npm i image-webpack-loader -D

使用:

{
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        exclude: resolve('src/icons'),
        type: 'asset',
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              disable: process.env.NODE_ENV !== 'production'
            },
          }
        ],
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024,
          },
        },
        generator: {
          filename: utils.assetsPath('img/[name].[hash:7][ext]')
        }
      }

经测试,以上会先经过image-webpack-loader,在通过webpack内置url-loader将图片小于设置maxSize的,就会转base64。所以会出现加了image-webpack-loader之后assets里面的图片变多的情况。 经此操作app.js大概能小几百KB到几MB不等,看读者图片质量以及数量。

npm i compression-webpack-plugin -D

配置:

// config=>index.js
·····
productionGzipExtensions: ['js', 'css']
······
// gzip开关
if (config.build.productionGzip) {
  const CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      filename: '[path][base].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

将代码进行gzip打包并配合nginx开启gzip模式,大幅度减小访问量。 nginx配置:

    #开启gzip
    gzip  on;
    #读取已经压缩好的文件(文件名为加.gz)
    gzip_static  on;
    #低于1kb的资源不压缩
    gzip_min_length 1k;
    #压缩级别【1-9】,越大压缩率越高,同时消耗cpu资源也越多,建议设置在4左右。
    gzip_comp_level 3;
    #需要压缩哪些响应类型的资源,多个空格隔开。不建议压缩图片,下面会讲为什么。
    gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
    #配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
    gzip_disable "MSIE [1-6]\.";
    #是否添加“Vary: Accept-Encoding”响应头
    gzip_vary on;

其中gzip_static on;是必不可少的。为了nginx直接读取.gz文件而不是通过服务端进行gzip压缩返回。 如果顺利的话能看到对比。

之前3.5MB: image.png gzip压缩之后仅有469KB:

image.png 如何看gzip是否生效以及是通过服务器压缩的还是直接拿.gz返回的可以通过Content-Encoding以及ETag前是否有W/

image.png

image.png 压缩之后文件请求大小直接小了一个量级,webpack,gzip章节我将重新写一篇完整介绍。期待一下。

总结:

缩小包体积部分总结:

笔者使用webpack-bundle-analyzer进行观察分析包体大小,并找出主要的大模块。通过以下方式进行优化:

  1. 使用externals排除大的模块,并通过cdn导入。
  2. 使用image-webpack-loader进行图片压缩
  3. 使用compression-webpack-plugin配合nginx压缩文件 以上五种方式,可以有效减小页面请求时候的文件大小。

构建速度部分总结(重复一遍):

笔者使用了speed-measure-webpack-plugin进行查看分析构建时主要loader和plugins的耗时,并通过:

  1. TerserPlugin.terserMinify替换为esbuild方式(减少13s左右)
  2. include exclude限制构建目标(效果不明显,减少1s不到)
  3. babel-loader通过cacheDirectory增加缓存,减少第二次构建时间(减少6秒)。
  4. 通过cache-loader缓存,减少主要耗时的loader的时间(减少4秒)。
  5. 官方推荐cashe(笔者未使用) 以上五种方式,让构建时间间从最初的一分钟左右减少到目前34秒。突然感觉时间都多了起来。

希望读者有所收获。若是有一些新方法可以在留言板告诉作者,作者将尝试后更新文章。 下一章节作者打算详细介绍compression-webpack-plugin以及对应配置。

笔者写技术文章分享不易,打包了百次总结测试得出,但是读者点赞了那将是我最大的动力啊喂。 对webpack从3升级到5感兴趣的可以看我的前置文章

将公司旧webpack3升级到5,踩坑总结

就是听说点赞会加薪、升职、变帅、变美欸~