webpack进阶篇

232 阅读7分钟

前言

紧接上一篇webpack入门篇,继续学习。继续敲!敲!敲!

1.优化构建速度

优化构建速度主要的目的是为了提高开发效率减少开发的等待时间,从而提高开发者体验

1.1 resolve(解析)

传送门

1.1.1 alias

alias用于创建 import 或 require 的别名,来确保模块引入变得更简单。其实配置别名对构建速度的影响微乎其微,主要是避免开发时路径混淆。项目中存在大量的模块引用,并且这些引用使用了长路径或复杂的相对路径时,Webpack在构建过程中需要逐个解析和查找这些模块的路径,这会导致构建过程中的性能下降。

// webpack.common.js
module.exports = {
  ...
  resolve: {
    alias: {
      '@': path.resolve(__dirname, '../src'),
    },
  },
};

使用时就可以通过别名来引入外部资源。

// index.js
import '@/css/index.css';
import '@/css/index.less';
import '@/css/index.scss';
import '@/assets/fonts/iconfont.css';
import App from '@/App.vue';
...

1.1.2 extensions

传送门

extensions配置项用于配置模块的文件扩展名,指示Webpack在引入模块时可以忽略文件的扩展名。

extensions配置项可能在某些情况下对构建速度产生一些影响。具体而言,当项目中存在大量模块引用时,如果每个引用都需要显式指定扩展名,Webpack在构建过程中需要逐个解析和查找这些模块文件的扩展名。这可能导致构建速度的下降,因为需要进行更多的文件系统操作和查找操作。

// webpack.common.js
module.exports = {
  resolve: {
    ...
    extensions: ['.js', '.vue', '.json'],
  },
};

在引入资源的时候就可以省略扩展名了。

// index.js
import '@/css/index.css';
import '@/css/index.less';
import '@/css/index.scss';
import '@/assets/fonts/iconfont.css';
import App from '@/App';

1.2 多进程构建

传送门

使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。

注意:仅在耗时的操作中使用此loader

npm install thread-loader -D

开启多进程之前,打包用时如下:

image.png

配置webpack开启多个进程用来处理babel-loader

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              worker: 4,// 产生worker的数量
              workerParallelJobs: 50,// 一个 worker 进程中并行执行工作的数量,默认20个
            },
          },
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        ],
      },
    ],
  },
};

再次打包,发现效果不是很明显。(应该是工程小、依赖少的原因)

image.png

1.3 使用缓存

传送门

cache在开发模式中被设置成type:'memory',在生产模式中被禁用。缓存有两种缓存:

  • memory:内存缓存(存储在内存中)
  • filesystem:文件系统缓存(存储在磁盘中)

当开启缓存时,首次打包webpack会根据项目的配置对源代码进行编译、转换和打包,生成最终的输出文件。并且缓存起来。当修改文件后进行第二次打包,webpack会根据先前缓存的结果进行比对,有缓存部分就在缓存中拿,没有缓存部分则进行构建大大提升了构建速度。

// webpack.prod.js
const commonConfig = require('./webpack.common');
const { merge } = require('webpack-merge');

module.exports = merge(commonConfig, {
  mode: 'production',
  cache: {
    type: 'filesystem',
  },
});

开启缓存之后,首次打包用时如下:

image.png

修改index.js文件,再次打包。

// index.js
import './css/index.css';
import './css/index.less';
import './css/index.scss';
import './assets/fonts/iconfont.css';
import App from './App.vue';
import Vue from 'vue';

console.log('Hello Webpack!');
+ console.log('cache');
console.log([1, 2, 3].map((n) => n + 1));

new Vue({
  render: (h) => h(App),
}).$mount('#app');

第二次打包构建的速度明显提升

image.png

1.4 externals(外链)

传送门

防止将某些 import 的包打包到 bundle 中,而是在运行时再去从外部获取这些扩展依赖。从而减少构建文件体积加快构建速度

例如:我们将Vue通过外链的方式在运行时从外部获取。

外链之前打包速度与产物大小如下:

image.png

image.png

卸载掉vue

npm uninstall vue

免费的CDN网站上找到对应版本的vue外链,粘贴到我们的HTML模板中。也可以使用html-webpack-plugin插件动态插入外链,这里就不再演示。如果想了解传送门

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><%= htmlWebpackPlugin.options.title %></title>
+ <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.min.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

配置webpack

// webpack.common.js
module.exports = {
  ...
  externals: {
    vue: 'Vue',
  },
};

外链之后打包速度与产物大小如下:

image.png

image.png

1.5 精确loader应用范围

在webpack配置中,includeexclude是用于指定应该应用或排除Loader的文件或目录的配置选项。这样可以避免对不必要的文件进行处理,提高构建性能和减少不必要的资源消耗。

因为之前的演示中我都是使用了exclude,所以为了做对比构建速度我先进行注释后打包:

image.png

配置webpack,指定babel-loader的应用范围

// webpack.common.js
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, '../src'),
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            options: {
              worker: 4,
              workerParallelJobs: 50,
            },
          },
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        ],
      },
    ],
  },
};

image.png

1.6 esbuild-loader

传送门

esbuild是一个用Go语言编写的快速JavaScript构建工具,具有出色的转译性能,通常比Babel要快一些。并且也能够做JS兼容,那么问题就来了babel-loaderesbuild-loader我该用哪个呢?下面就看看它们各自的优势吧!

esbuild-loader优势:

  • 极快的构建速度
  • 低资源消耗
  • 适合现代语法

babel-loader优势:

  • 更广泛的语法支持
  • 大型生态系统
  • 定制性

所以如果是在开发中我会选择babel-loader。

使用babel-loader构建

image.png

npm install esbuild-loader -D

配置webpack

// webpack.common.js
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'esbuild-loader',
        include: path.resolve(__dirname, '../src'),
        exclude: /node_modules/,
        options: {
          target: 'es2015',
          loader: 'js',
        },
        // use: [
        //   {
        //     loader: 'thread-loader',
        //     options: {
        //       worker: 4,
        //       workerParallelJobs: 50,
        //     },
        //   },
        //   {
        //     loader: 'babel-loader',
        //     options: {
        //       presets: ['@babel/preset-env'],
        //     },
        //   },
        // ],
      },
    ],
  },
};

image.png

2.构建产物优化

2.1 压缩CSS

传送门

CssMinimizerWebpackPlugin是一个用于压缩和优化CSS代码的Webpack插件。

npm install css-minimizer-webpack-plugin -D

使用插件前

image.png

配置webpack

// webpack.prod.js
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = merge(commonConfig, {
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [new CssMinimizerPlugin()],
  },
});

image.png

2.2 压缩JS

传送门

webpack v5 开箱即带有最新版本的 terser-webpack-plugin。这是一个用于压缩和优化JavaScript代码的Webpack插件。它使用Terser进行代码压缩和混淆,旨在减小JavaScript文件的体积,提高加载速度,并优化用户体验。

压缩前:

image.png

// webpack.prod.js
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(commonConfig, {
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [new CssMinimizerPlugin(), new TerserPlugin()],
  },
});

image.png

2.3 压缩CSS&JS

传送门

esbuild-loader中的EsbuildPlugin插件不但能够压缩JS还能够压缩CSS

// webpack.prod.js
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');
const { EsbuildPlugin } = require('esbuild-loader');
// const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
// const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(commonConfig, {
  ...
  optimization: {
    minimize: true,
    minimizer: [
//      new CssMinimizerPlugin(),
//      new TerserPlugin(),
      new EsbuildPlugin({
        target: 'es2015',
        css: true, // 压缩CSS
      }),
    ],
  },
});

image.png

2.4 压缩图片

传送门

image-minimizer-webpack-plugin是用于压缩图片的插件,这个插件可以使用2个工具来压缩图像:imageminsquoosh

图片还有两种模式优化:有损压缩无损压缩

推荐imagemin插件进行无损优化

npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D

推荐的有损优化imagemin插件

npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo -D

因为项目中只有一张jpg格式的图片,所以就只用到imagemin-jpegtran进行处理。

npm install image-minimizer-webpack-plugin imagemin -D
// 使用–ignore-scripts,则会让npm避免执行package.json文件中的scripts脚本
npm install imagemin-jpegtran --ignore-scripts -D 

压缩前图片大小如下:

image.png

配置webpack

// webpack.prod.js
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

module.exports = merge(commonConfig, {
  ...
  // webpack官网上是将插件放plugin中,插件的github上是放optimization中。亲自试验了一下都可以。
  optimization: {
    minimize: true,
    minimizer: [
      new ImageMinimizerPlugin({
        minimizer: {
          // 官网上是使用,imageminMinify但是试验发现不能生成jpg。使用imageminGenerate可以。
          implementation: ImageMinimizerPlugin.imageminGenerate,
          options: {
            plugins: [['jpegtran', { progressive: true }]],
          },
        },
      }),
    ],
  },
});

运行打包命令时,发现如下报错。说jpegclub.exe不存在。

image.png

通过百度后得到了解决方法,只需要去官网上将jpegclub.exe下载下来并拖入vendor目录下即可,下载地址

image.png

下载完成后,将其拖入vendor

image.png

然后再次执行打包命令:

image.png

2.5 清除无用代码

在项目打包上线的时候,项目中通常会有一些无用的代码。而这些代码会增加打包后的文件体积,不利于浏览器资源的加载。

无用的代码有哪些?

  • 调试的代码:console、debugger
  • 代码注释:在运行时我们不需要代码注释
  • 无用的变量和函数:项目中定义了但是并没有使用到

去除调试代码

const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(commonConfig, {
  ...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,// 去除所有的console代码,需要手动开启
            drop_debugger: true,// 去除调试debugger,production模式下自动开启
            //pure_funcs: ['console.log'],// 删除指定代码,如果需要打印在控制台可以使用console.info
          },
        },
      }),
    ],
  },
});

webpack5下production模式下默认去除代码注释,默认会通过Tree Shaking去除掉没有被使用的变量和函数,无须进行配置。

2.6 代码分包

传送门

splitChunks用于将公共模块(公共依赖)提取出来,以便在多个入口文件之间共享和复用。它可以帮助减少重复的模块加载,提高应用程序的性能和加载速度。

optiondefaultdescription
chunksasync指定需要进行模块拆分的范:async(异步加载模块拆分)、initial(同步模块导入拆分)、all(所有模块拆分)
minSize20000指定一个模块的最小大小,只有超过这个大小的模块才会被拆分
minChunks1指定一个模块在多少个入口文件中被引用时,才会被认为是公共模块
maxAsyncRequests30指定异步加载的模块并行请求的最大数量
maxInitialRequests30指定初始入口模块并行请求的最大数量
cacheGroups-定义缓存组,允许对不同的模块进行不同的配置

配置webpack

// webpack.prod.js
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common');

module.exports = merge(commonConfig, {
  ...
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      minChunks: 1,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: -10,// 优先级
        },
        common: {
          name: 'common',
          minChunks: 2,
          priority: -20,
          // 如果当前chunk包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块。
          reuseExistingChunk: true,
        },
      },
    },
  },
});

image.png