聊一聊 webpack5性能优化有哪些?

介绍

此文章基于webpack5来阐述

webpack性能优化较多,可以对其进行分类

  1. 优化打包速度,开发或者构建时优化打包速度(比如exclude、catch等)
  2. 优化打包后的结果,上线时的优化(比如分包处理、减小包体积、CDN服务等)

大多数情况下,我们会更侧重于优化打包后的结果,这对于线上产品的影响更大。

优化构建速度

对于 优化构建速度 会从 定向查找减少执行构建的模块合理使用缓存并行构建以提升总体速度并行压缩提高构建效率几个方面入手。

定向查找

resolve.modules

webpackresolve.modules 配置用于指定 webpack 去哪些目录下寻找第三方模块。其默认值是 ['node_modules']webpack 在寻找的时候,会先去当前目录的 ./node_modules 下去查找,没有找到就会再去上一级目录 ../node_modules 中去找,直到找到为止。

所以如果我们项目的第三方依赖模块放置的位置没有变更的话,可以使用绝对路径减少查找的时间,配置如下:

js 复制代码
module.export = {
  resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    // __dirname 表示当前工作目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')]
  }
}

resolve.extensions

extensions 是我们常用的一个配置,适用于指定在导入语句没有带文件后缀时,可以按照配置的列表,自动补上后缀。我们应该根据我们项目中文件的实际使用情况设置后缀列表,将使用频率高的放在前面、同时后缀列表也要尽可能的少,减少没有必要的匹配。同时,我们在源码中写导入语句的时候,尽量带上后缀,避免查找匹配浪费时间。

同时,我们在源码中写导入语句的时候,尽量带上后缀,避免查找过程。

js 复制代码
module.export = {
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  }
}

extensions的默认值是['.js', '.json', '.wasm'],所以不写文件后缀的话,它会依次匹配'.js', '.json', '.wasm',如果都没匹配上的话才会报错。

减少执行构建的模块

合理配置 loader 的 include、exclude

loader 对文件的转换是个耗时的操作,并且 loader 的配置会批量命中多个文件,所以需要根据自己的项目尽可能的精准命中哪些文件是需要被 loader 处理的。

webpack 提供了 test、include、exclude 三个配置项来命中 loader 。

比如,我们只想对根目录 src 下的 js 文件使用 babel-loader 进行处理可以这样设置:

js 复制代码
const path = require('path');
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /.jsx?$/,
        use: ['babel-loader'],
        include: [path.resolve(__dirname, 'src')]
      }
    ]
  },
}

或者我们想排除node_modules目录下的js使用 babel-loader 进行处理可以这样设置:

js 复制代码
const path = require('path');
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /.jsx?$/,
        use: ['babel-loader'],
        exclude: /node_modules/, //排除 node_modules 目录
      }
    ]
  },
}

合理使用缓存

在优化的方案中,缓存也是其中重要的一环。在构建过程中,可以通过使用缓存提升二次打包速度。

babel-loader 开启缓存

cacheDirectory 的默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程。

js 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /.jsx?$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,
            },
          }
        ]
      }
    ]
  }
}

cache-loader

没有缓存配置的loader该怎么使用缓存呢?那就得借助 cache-loader

其使用如下:

js 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /.jsx?$/,
        use: [
          'cache-loader', //需要安装
          "babel-loader"
        ],
      }
    ]
  }
}

webpack5 配置 cache.type

webpack5新增的cache属性,可以开启磁盘缓存,默认将编译结果缓存在 node_modules/.cache/webpack目录下。

cache 会在开发 模式被设置成 type: 'memory' 而且在 生产 模式中被禁用。 cache: truecache: { type: 'memory' } 配置作用一致。 传入 false 会禁用缓存。

经过配置,再次构建可以看到.cache/webpack 下生成了缓存文件

当然缓存也是不能盲目使用,也是需要斟酌,因为保存和读取这些缓存文件也会有一些时间开销,所以建议只对性能开销较大的 loader 采用改缓存优化。

并行构建以提升总体速度

默认情况下,webpack 是单线程模型,一次只能处理一个任务,在文件过多时会导致构建速度变慢。所以在减少了需要执行构建的模块和降低了单个模块的构建速度之外,我们还可以并行构建,让 webpack 同时处理多个任务,发挥多核 CPU 的优势。

Thread-loader

Thread-loader ,会创建多个 worker 池进行并发执行构建任务,但是使用起来更为简单。只要将这个 loader 放置在其他 loader之前, 放置在这个 Thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool) 中运行。

其使用如下:

js 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /.jsx?$/,
        use: [
          // 开启多进程打包。 
          {
            loader: 'thread-loader', // 需要安装
            options: {
              workers: 3 // 进程3个
            }
          },
          {
            loader: 'babel-loader',
          }
        ]
      }
    ]
  }
}

webpack 官网 中也有提示,每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。所以建议仅在耗时的 loader 上使用。

只有在代码量很多的时候开启多进程构建才会有明显的提升,如果项目很简单,代码量少可能会适得其反。所以使用前需要斟酌,不要为了优化而优化。

并行压缩提高构建效率

前面说了并行构建,下面来说说并行压缩。

TerserWebpackPlugin 开启 paralle

js 复制代码
module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({ parallel: true }), // 默认已经开启,其实无需设置
    ],
  },
};

优化构建结果

对于 优化构建结果 我们可以从 压缩代码按需加载预加载Code SplittingTree ShakingGzip作用提升几个方面入手。

压缩代码

压缩 html

压缩 html 使用的还是 html-webpack-plugin 插件。该插件支持配置一个 minify 对象,用来配置压缩 html

js 复制代码
module.export = {
  plugins: [
    new HtmlWebpackPlugin({
      // 动态生成 html 文件
      template: "./index.html",
      minify: {
        // 压缩HTML
        removeComments: true, // 移除HTML中的注释
        collapseWhitespace: true, // 删除空⽩符与换⾏符
        minifyCSS: true // 压缩内联css
      },
    })
  ]
}

压缩 css

webpack5中推荐使用的是 css-minimizer-webpack-plugin

首先我们在入口文件里面引入test.css文件

js 复制代码
import "./test.css";

test.css文件内容如下

css 复制代码
.box {
  background-color: red;
}

.item {
  color: black;
}

配置方式:

js 复制代码
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

optimization: {
  // 是否需要压缩
  minimize: true, // 需要开启
  // 配置压缩工具
  minimizer: [
    // 添加 css 压缩配置
    new CssMinimizerPlugin({}), // 需要安装
  ],
},

重新构建,会单独生成了 main.css 文件,并对css进行了压缩。

压缩 js

目前的主流还是terser-webpack-plugin,在webpack5生产环境中(mode=production),已默认开启。

如果你不想使用terser-webpack-plugin插件的默认配置,想自定义,也是支持的,可以直接 引入terser-webpack-plugin进行自定义配置即可。

js 复制代码
const TerserPlugin = require("terser-webpack-plugin"); // webpack5内置,不需要再单独安装

optimization: {
  // 是否需要压缩
  minimize: true,
  // 配置压缩工具
  minimizer: [
    new TerserPlugin({// 在这里自定义配置}),
  ],
},

按需加载

很多时候不需要一次性加载所有的JS文件,而应该在不同阶段去加载所需要的代码。webpack内置了强大的分割代码的功能可以实现按需加载。

比如,我们在点击了某个按钮之后,才需要使用使用对应的JS文件中的代码,我们可以使用 import() 语法按需引入

js 复制代码
// index.js

document.getElementById('btn1').onclick = function() {
  import('./impModule.js').then(fn => fn.default());
}

impModule.js

js 复制代码
export default () => {
  console.log("我是懒加载模块");
};

我们打包之后,动态加载的模块会单独生成一个js文件。

页面首次加载的时候并不会加载该js文件,而是当我们需要使用到的时候才会进行加载。我们在页面上看看效果。

在默认情况下打包出来的文件名是路径的组合,比如上面的src_impModule.js,如果你不想使用这个名字,想通俗易懂可以在import里面配置 webpackChunkName魔法注释。

比如上面的例子,我们配置ebpackChunkName: "btnChunk"

js 复制代码
// index.js

document.getElementById('btn1').onclick = function() {
  import(/* webpackChunkName: "btnChunk" */ './impModule.js').then(fn => fn.default());
}

再次构建可以看到,构建出来的文件名就是我们事先定义好的名称

预加载(prefetch 和 preload)

上面说的代码懒加载在使用的时候才去加载是会提升页面性能,但是如果懒加载的模块比较大,当点击的时候再去加载的话无疑会让用户等待时间加长。

可以利用浏览器空闲时候去加载这些切分出来的模块,那就是prefetch 和 preload

prefetch和preload的概念

prefetch(预取):将来可能需要一些模块资源,在核心代码加载完成之后浏览器空闲的时候再去加载需要用到的模块代码。

preload(预加载):当前核心代码加载期间可能需要模块资源,其是和核心代码文件一起去加载的。

prefetch

我们将上面的例子稍微改下,加个注释/* webpackPrefetch: true */

js 复制代码
// index.js

document.getElementById("btn1").onclick = async () => {
  const imp = await import(/* webpackPrefetch: true */ "./impModule.js");
  imp.default();
};

上面的代码的意思是当我们主要的核心代码加载完成,浏览器有空闲的时候,浏览器就会帮我们自动的去下载impModule.js

head里面,懒加载模块会被直接引入了,并且会添加rel='prefetch'

preload

prefetch 与 preload 的区别

  1. preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  2. preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  3. preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  4. 浏览器支持程度不同,需要注意。

Code Splitting (代码分割)

要理解webpack的提出的几个概念,module、chunk和bundle。

module:每个import引入的文件就是一个模块

chunk:当module源文件传到webpack进行打包时,webpack会根据文件引用关系生成chunk

bundle:是对chunk进行压缩、分割等处理后的产物

SplitChunksPlugin

可以阅读我的另外一篇文章,里面讲述了一些拆包策略SplitChunksPlugin

MiniCssExtractPlugin

可以利用 mini-css-extract-plugin 插件,将我们的css代码分离出来。

接下来我们实操下。

创建样式文件

css 复制代码
// index.less
.a {
  background-color: aqua;
}

.b {
  font-size: 18px;
}

配置mini-css-extract-plugin插件

js 复制代码
//webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

// ...
{
  test: /.less?$/,
  use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"],
  exclude: /node_modules/, //排除 node_modules 目录
},

plugins: [
  // ...
  new MiniCssExtractPlugin(),
  }),
],

css代码的分离优化原理其实和js是一样的。第一就是拆出来利用浏览器并发请求特性进行快速加载,其次就是多页面如果用到了相同样式能进行复用。

注意,此插件为每个包含 cssjs 文件创建一个单独的 css 文件

这句话的意思就是,css代码的分割不是以引入了多少个css文件构建后就有多少个css文件,而是根据你的js包来的,如果构建后你的js包只有一个那么css包也只会有一个,而不管你源代码里引入了多少个css文件。

Tree Shaking

利用 ES Module 静态分析的特点来检测模块内容的导出、导入以及被使用的情况,保留被使用的代码,消除不会被执行和没有副作用(Side Effect) 的 死代码。

Tree Shaking 最先出现在rollup中,rollup 是在编译打包过程中分析程序流,得益于 ES6 静态模块(exports 和 imports 不能在运行时修改),我们在打包时就可以确定哪些代码是我们需要的。

webpack 本身在打包时只能标记未使用的代码而不移除,而识别代码未使用标记并完成 tree-shaking 的 其实是 UglifyJS、terser 这类压缩代码的工具。简单来说,就是压缩工具读取 webpack 打包结果,在压缩之前移除 bundle 中未使用的代码

webpack 中的 sideEffects 表示是否要开启识别 package.json 中 sideEffects 标记的功能,而 package.json 中的 sideEffects 是为了告知打包工具该模块是否包含副作用或者包含哪些副作用。

Gzip

前端除了在打包的时候将无用的代码或者 console、注释剔除之外。还可以使用 Gzip 对资源进行进一步压缩。

  1. 当用户访问 web 站点的时候,会在 request header 中设置 accept-encoding: gzip,表明浏览器是否支持 Gzip
  2. 服务器在收到请求后,判断如果需要返回 Gzip 压缩后的文件那么服务器就会先将我们的 JS\CSS 等其他资源文件进行 Gzip 压缩后再传输到客户端,同时将 response headers 设置 content-encoding:gzip。反之,则返回源文件。
  3. 浏览器在接收到服务器返回的文件后,判断服务端返回的内容是否为压缩过的内容,是的话则进行解压操作。
js 复制代码
const CompressionWebpackPlugin = require("compression-webpack-plugin"); // 需要安装

module.exports = {
  plugins: [
    new CompressionWebpackPlugin()
  ]
}

配置好我们再来构建,会生成资源的.gz文件

当然在Nginx上也可以在配置文件中启用 gzip 压缩

作用提升 (Scope Hoisting)

Scope Hoisting 作用域提升,在 JavaScript 中,也有类似的概念,"变量提升"、"函数提升",JavaScript 会把函数和变量声明提升到当前作用域的顶部,Scope Hoisting 也是类似。webpack 会把引入的 js 文件"提升"顶部。

在没有使用 Scope Hoisting 的时候,webpack 的打包文件会将各个模块分开使用 __webpack_require__ 导入,在使用了 Scope Hoisting 之后,就会把需要导入的文件直接移入使用模块的顶部。这样做的好处有

  • 代码中函数声明和引用语句减少,减少代码体积
  • 不用多次使用 __webpack_require__ 调用模块,运行速度会的得以提升。

所以,Scope Hoisting 可以让 webpack 打包出来的代码文件体积更小,运行更快。

因为 Scope Hoisting 需要分析模块之间的依赖关系,所以源码必须采用 ES6 模块化语法。也就是说如果你使用非 ES6 模块或者使用 import() 动态导入的话,则不会有 Scope Hoisting

Scope Hoistingwebpack 内置功能,只需要在plugins里面使用即可

js 复制代码
module.exports = {
  plugins: [
    // 开启 Scope Hoisting 功能
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
}

不过生产环境下 Scope Hoisting 功能是默认开启的,不用再额外处理。

常用分析工具

时间分析工具 speed-measure-webpack-plugin

speed-measure-webpack-plugin 这个插件分析整个打包的总耗时,以及每一个loader 和每一个 plugins 构建所耗费的时间,从而帮助我们快速定位到可以优化 Webpack 的配置。

使用如下

js 复制代码
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); // 需要安装
const smp = new SpeedMeasurePlugin();

module.exports = () => smp.wrap(config); // 使用smp包裹webpack的配置

构建结果分析工具 webpack-bundle-analyze

webpack-bundle-analyzer 能可视化的反映

  1. 打包出的文件中都包含了什么;
  2. 每个文件的尺寸在总体中的占比,哪些文件尺寸大;
  3. 模块之间的包含关系;
  4. 是否有重复的依赖项,是否存在一个库在多个文件中重复? 或者包中是否具有同一库的多个版本?
  5. 每个文件的压缩后的大小。

使用如下:

js 复制代码
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // 需要安装

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}
相关推荐
y先森4 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy4 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189114 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿5 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡6 小时前
commitlint校验git提交信息
前端
虾球xz7 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇7 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒7 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员7 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐7 小时前
前端图像处理(一)
前端