Skip to content

Webpack5-高级-性能优化

[TOC]

webpack性能优化

概述

webpack 作为前端目前使用最广泛的打包工具,在面试中也是经常会被问到的。

比较常见的面试题包括:

  • 可以配置哪些属性来进行 webpack 性能优化?

  • 前端有哪些常见的性能优化?(问到前端性能优化时,除了其他常见的,也完全可以从 webpack 来回答)

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

  • 优化一:打包后的结果,上线时的性能优化。(比如分包处理减小包体积CDN 服务器等)

  • 优化二:优化打包速度,开发或者构建时优化打包速度。(比如 excludecache-loader 等)

大多数情况下,我们会更加侧重于优化一,这对于线上的产品影响更大。

在大多数情况下 webpack 都帮我们做好了该有的性能优化:

  • 比如配置 mode 为 production 或者 development 时,默认 webpack 的配置信息;

  • 但是我们也可以针对性的进行自己的项目优化;

接下来,我们来学习一下 webpack 性能优化的更多细节。

分包处理

概述

代码分离(Code Splitting)是 webpack 一个非常重要的特性:

  • 它主要的目的是将代码分离到不同的 bundle 中,之后我们可以按需加载,或者并行加载这些文件

  • 比如默认情况下,所有的 JavaScript 代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度;

  • 代码分离可以分出更小的 bundle,以及控制资源加载优先级,提高代码的加载性能;

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

  • 多入口起点:使用 entry 配置手动分离代码;
  • 动态导入:通过模块的内联函数调用来分离代码;
  • SplitChunks:使用 Entry Dependencies 或者 SplitChunksPlugin 去重和分离代码;

多入口起点

多入口起点的含义非常简单,就是配置多入口:

  • 比如配置一个 index.js 和 main.js 的入口;

  • 他们分别有自己的代码逻辑;

js
 module.exports = {
  mode: 'development',
  entry: {
+    index: './src/index.js',
+    main: './src/main.js',
  },
   output: {
+    filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

打包后的文件:

sh
index.bundle.js
main.bundle.js

缺点: 这种方式存在一些隐患:

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

防止入口依赖重复

假如我们的 index.js 和 main.js 都依赖两个库:lodash、dayjs

  • 如果我们单纯的进行入口分离,那么打包后的两个 bunlde 都有会有一份 lodash 和 dayjs;

  • 事实上我们可以对他们进行共享;

在配置文件中配置 dependOn 选项,以在多个 chunk 之间共享模块:

js
 const path = require('path');

 module.exports = {
   mode: 'development',
   entry: {
    index: {
      import: './src/index.js',
+      dependOn: 'shared1',
    },
    main: {
      import: './src/main.js',
+      dependOn: 'shared1',
    },
+    shared1: ['axios', 'lodash'],
+    shared2: ['react', 'vue'],
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };

动态导入

另外一个代码拆分的方式是动态导入(dynamic import),webpack 提供了两种实现动态导入的方式

  • 第一种,使用 ECMAScript 中的 import() 语法来完成,也是目前推荐的方式;

  • 第二种,使用 webpack 遗留的 require.ensure,目前已经不推荐使用;

比如我们有一个模块 bar.js:

  • 该模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载);

  • 因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的 js 文件;

  • 这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的 js 代码;

  • 这个时候我们就可以使用动态导入;

1、点击按钮时动态导入模块

image-20240302181231050

2、打包结果:

image-20240302181414694

3、只有在用到时,包才会被下载下来

image-20240302181600440

注意: 使用动态导入 bar.js:

  • 在 webpack 中,通过动态导入获取到一个对象;

  • 真正导出的内容,在该对象的 default 属性中,所以我们需要做一个简单的解构;

image-20240304145732358

动态导入的文件命名

  • 因为动态导入通常是一定会打包成独立的文件的,所以并不会再 cacheGroups 中进行配置;

  • 那么它的命名我们通常会在 output 中,通过 chunkFilename 属性来命名;

1、使用[name]占位符

image-20240304150128925

2、使用[name][id]占位符

image-20240229141345643

但是,你会发现默认情况下我们获取到的 [name] 是和 id 的名称保持一致的

3、如果我们希望修改 [name] 的值,可以通过 magic comments(魔法注释)的方式;

image-20240229141355896

image-20240304150907582

splitChunks

基本使用

另外一种分包的模式是 自定义分包(splitChunks),它底层是使用 SplitChunksPlugin 来实现的:

  • 因为该插件 webpack 已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件;

  • 只需要提供 SplitChunksPlugin 相关的配置信息即可;

Webpack 提供了 SplitChunksPlugin 默认的配置,我们也可以手动来修改它的配置:

  • 比如默认配置中,chunks 仅仅针对于异步(async)请求,我们可以设置为 initial 或者 all;

image-20240229141254822

image-20240304153254799

常见配置
  • splitChunks:``,用于对代码进行分割和提取公共模块。可以将一些通用的模块抽离出来成为单独的文件,以减小打包文件的体积,提高加载速度。

    • chunksall | async | initial | ()=>{},指定哪些 chunk 参与分包
      • 'all':(),所有 chunk 参与分包
      • 'async':(默认),按需加载的异步 chunk 参与分包
      • 'initial':(),初始加载的 chunk 参与分包
      • ()=>{}:(),提供一个函数去做更多的控制。这个函数的返回值将决定是否包含每一个 chunk。
    • minSizenumber,指定分割后的 chunk 的最小大小,单位为 bytes。
    • maxSizenumber,指定分割后的 chunk 的最大大小,单位为 bytes。如果为 0,则表示没有限制。
    • cacheGroups{key: {test, priority, filename, reuseExistingChunk}, key: {}...},缓存组,用于配置不同类型的模块如何被分割。
      • testreg | function | string,用于匹配符合条件的模块。
      • prioritynumber,(默认:-20),决定模块被打包到哪个 cache group 的优先级,数值越大优先级越高。
      • filenamestring | (pathData, assetInfo) => string,(),给生成的文件分配名称。
      • reuseExistingChunkboolean,(默认:true),是否重用已经存在的 chunk。
  • chunkIdsfalse | 'natural' | 'named' | 'deterministic' | 'size' | 'total-size' ,告知 webpack 当选择模块 id 时需要使用哪种算法。

    • false:(默认),没有任何内置的算法。但自定义的算法会由插件提供。

    • named:(开发模式默认值),对调试更友好的可读的 id。(完整的名子)

    • deterministic:(生产模式默认),在不同的编译中不变的短数字 id。有益于长期缓存。(数字不会变化)

      • 在 webpack4 中是没有这个值的,那个时候如果使用 natural,那么在一些编译发生变化时,就会有问题
    • natural:(),按使用顺序的数字 id。(数字会变化)

    • size:(),专注于让初始下载包大小更小的数字 id。

    • total-size:(),专注于让总下载包大小更小的数字 id。

  • runtimeChunktrue | 'multiple' | 'single' | {name},配置 runtime 相关的代码是否抽取到一个单独的 chunk 中,目前已经不再单独打包

    • true | multiple:(),针对每个入口打包一个 runtime 文件
    • single:(),打包一个 runtime 文件
    • {name}:(推荐),name 属性决定 runtimeChunk 的名称
  • minimizer[new TerserPlugin()?, new CssMinimizer()?, '...'?],允许你通过提供一个或多个定制过的 TerserPlugin 实例,覆盖默认压缩工具(minimizer)。

    • new TerserPlugin()?( {parallel?, terserOptions?} ),用于对JS代码简化的插件
      • 依赖包:terser-webpack-plugin (内置插件)
      • parallel?boolean,是否开启多进程并行压缩
      • terserOptions?{},传递给 Terser 压缩器的选项
      • extractComments?boolean,(默认:true),是否提取注释
    • new CssMinimizer()?( {parallel?, minimizerOptions?} ),压缩和优化 CSS 代码的插件
      • 依赖包:css-minimizer-webpack-plugin (内置插件)
      • parallel?boolean,是否开启多进程并行压缩
      • minimizerOptions?{preset},传递给 CssMinimizer压缩器的选项,如指定压缩规则、是否启用并行压缩等
    • '...'?,可以使用 '...' 来访问默认值

示例:

js
optimization: {
  splitChunks: {
    chunks: 'all',
    minSize: 30000,
    maxSize: 0,
    cacheGroups: {
      defaultVendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10,
        name: 'vendors',
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true,
        name: 'common',
      },
    },
  },
}

示例: 我们可以自定义更多配置,我们来了解几个非常关键的属性:

image-20240229141307731

chunkIds
  • chunkIdsfalse | 'natural' | 'named' | 'deterministic' | 'size' | 'total-size' ,告知 webpack 当选择模块 id 时需要使用哪种算法。
    • false:(默认),没有任何内置的算法。但自定义的算法会由插件提供。

    • named:(开发模式默认值),对调试更友好的可读的 id。(完整的名子)

    • deterministic:(生产模式默认),在不同的编译中不变的短数字 id。有益于长期缓存。(数字不会变化)

      • 在 webpack4 中是没有这个值的,那个时候如果使用 natural,那么在一些编译发生变化时,就会有问题
    • natural:(),按使用顺序的数字 id。(数字会变化)

    • size:(),专注于让初始下载包大小更小的数字 id。

    • total-size:(),专注于让总下载包大小更小的数字 id。

最佳实践:

  • 开发过程中,我们推荐使用 named

  • 打包过程中,我们推荐使用 deterministic

image-20240822175828955

image-20240822180003620

runtimeChunk
  • runtimeChunktrue | 'multiple' | 'single' | {name},配置 runtime 相关的代码是否抽取到一个单独的 chunk 中,目前已经不再单独打包
    • true | multiple:(),针对每个入口打包一个 runtime 文件

    • single:(),打包一个 runtime 文件

    • {name}:(推荐),name 属性决定 runtimeChunk 的名称

runtime 相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码。比如我们的 component、bar 两个通过 import 函数相关的代码加载,就是通过 runtime 代码完成的

优点: 抽离出来后,有利于浏览器缓存的策略:

  • 比如我们修改了业务代码(main),那么 runtime 和 component、bar 的 chunk 是不需要重新加载的;

  • 比如我们修改了 component、bar 的代码,那么 main 中的代码是不需要重新加载的;

image-20240229141439870

image-20240304170547290

注释提取
  • minimizer[new TerserPlugin()?, new CssMinimizer()?, '...'?],允许你通过提供一个或多个定制过的 TerserPlugin 实例,覆盖默认压缩工具(minimizer)。
    • new TerserPlugin()?( {parallel?, terserOptions?} ),用于对JS代码简化的插件
      • 依赖包:terser-webpack-plugin (内置插件)
      • parallel?boolean,是否开启多进程并行压缩
      • terserOptions?{},传递给 Terser 压缩器的选项
      • extractComments?boolean,(默认:true),是否提取注释
    • new CssMinimizer()?( {parallel?, minimizerOptions?} ),压缩和优化 CSS 代码的插件
      • 依赖包:css-minimizer-webpack-plugin (内置插件)
      • parallel?boolean,是否开启多进程并行压缩
      • minimizerOptions?{preset},传递给 CssMinimizer压缩器的选项,如指定压缩规则、是否启用并行压缩等
    • '...'?,可以使用 '...' 来访问默认值

示例: 配置CssMinimizerPlugin选项

js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new CssMinimizerPlugin({
  		minimizerOptions: {
    	  preset: ['default', { discardComments: { removeAll: true } }],
  		},
 	    parallel: true,
	  }),
    ],
  },
};

示例: 配置TerserPlugin选项

js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          ecma: 6,
        },
      }),
    ],
  },
};

默认情况下,webpack 在进行分包时,有对包中的注释进行单独提取。

image-20240229141319076

这个包提取是由另一个插件 TerserPlugin 默认配置的原因:

image-20240229141326972

注意: 当使用pnpm导入terser-webpack-plugin包时,会出现导入不了包的问题。这是因为该包不是存在于node_modules的根目录下,而是在webpack包目录中。这个时候就需要手动安装该包:

sh
pnpm i terser-webpack-plugin -D
懒加载

动态 import 使用最多的一个场景是懒加载(比如路由懒加载):

  • 封装一个 component.js,返回一个 component 对象;

  • 我们可以在一个按钮点击时,加载这个对象;

image-20240229141407621

image-20240229141414837

prefetch、preload

webpack v4.6.0+ 增加了对预获取和预加载的支持。

在声明 import 时,使用下面这些内置指令,来告知浏览器:

  • prefetch(预获取):(推荐),将来某些导航下可能需要的资源

  • preload(预加载):当前导航下可能需要资源

prefetch对比preload:

与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载

    prefetch chunk 会在父 chunk 加载结束后开始加载

  • preload chunk 具有中等优先级,并立即下载。

    prefetch chunk 在浏览器闲置时下载

  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。

    prefetch chunk 会用于未来的某个时刻。

使用方法: 通过添加魔法注释来使用

  • prefetch: /* webpackPrefetch: true */
  • preload: /* webpackPreload: true */

image-20240229141453889

效果:

image-20240304180416388

CDN

什么是 CDN?

CDN 称之为内容分发网络(Content Delivery Network 或 Content Distribution Network,缩写:CDN)

  • 它是指通过相互连接的网络系统,利用最靠近每个用户的服务器;

  • 更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户;

  • 来提供高性能、可扩展性及低成本的网络内容传递给用户;

image-20240229141507297

在开发中,我们使用 CDN 主要是两种方式:

  • 方式一:打包的所有静态资源,放到 CDN 服务器,用户所有资源都是通过 CDN 服务器加载的;

  • 方式二:一些第三方资源放到 CDN 服务器上;

购买 CDN 服务器

  • output:输出
    • publicPathstring | function,指定在浏览器中引用打包后的资源时的公共路径。
      • 用处:
        • 处理静态资源文件(如图片、字体、样式表等)在 HTML 中的引用路径。如publicPath: /mr-trip/
        • 指定静态资源文件在 CDN 上的路径。如:https://www.xxx.com/cdn/

如果所有的静态资源都想要放到 CDN 服务器上,我们需要购买自己的 CDN 服务器;

1、目前阿里、腾讯、亚马逊、Google 等都可以购买 CDN 服务器;

2、我们可以直接修改 publicPath,在打包时添加上自己的 CDN 地址;

image-20240229141537831

image-20240229141546853

第三方库的 CDN 服务器

  • externals {key: value} | string | function | reg |,从输出的 bundle 中排除依赖
    • key,排除的框架的名称
    • value,从CDN地址请求下来的JS中提供的对应的名称(不能乱写)

通常一些比较出名的开源框架都会将打包后的源码放到一些比较出名的、免费的 CDN 服务器上:

CDN基本使用

在项目中,我们如何去引入这些 CDN 呢?

1、在打包的时候我们不再需要对类似于 lodash 或者 dayjs 这些库进行打包。通过 webpack 配置,来排除一些库的打包

  • key:排除的框架的名称
  • value:从CDN地址请求下来的JS中提供的对应的名称(不能乱写)

image-20240229141559727

image-20240305121853340

2、在 html 模板中,我们需要自己加入对应的 CDN 服务器地址;

image-20240229141605048

image-20240305122214274

Shimming

  • new ProvidePlugin()({key: value}),用于自动加载模块而不必在每个模块中显式地 importrequire 它们

    • keystring,希望在全局作用域中提供的变量名称。

    • valuestring | [moduel, variable],要加载的模块

      • modulestring,要加载的模块(通常是 npm 包)。
      • variablestring,可选,指定模块中的具体变量。如果你要加载整个模块,可以省略这个选项。

概述

shimming 是一个概念,是某一类功能的统称:

  • shimming 翻译过来我们称之为 垫片相当于给我们的代码填充一些垫片来处理一些问题

  • 比如我们现在依赖一个第三方的库,这个第三方的库本身依赖 lodash,但是默认没有对 lodash 进行导入(认为全局存在lodash),那么我们就可以通过 ProvidePlugin 来实现 shimming 的效果;

注意: webpack不推荐随意使用shimming!webpack 背后的整个理念是使前端开发更加模块化。也就是说,需要编写具有良好的封闭性(well contained)、不依赖于隐含依赖(例如,全局变量)的彼此隔离的模块。请只在必要的时候才使用这些特性。

预制全局变量

目前我们的 lodash、dayjs 都使用了 CDN 进行引入,所以相当于在全局是可以使用_和 dayjs 的

  • 假如一个文件中我们使用了 axios,但是没有对它进行引入,那么下面的代码是会报错的;

image-20240903155633361

image-20240903155654878

我们可以通过使用 ProvidePlugin 来实现 shimming 的效果:

  • ProvidePlugin 能够帮助我们在每个模块中,通过一个变量来获取一个 package;

  • 如果 webpack 看到这个模块,它将在最终的 bundle 中引入这个模块;

  • 另外 ProvidePlugin 是 webpack 默认的一个插件,所以不需要专门导入;

image-20240903155903960

image-20240305131705608

这段代码的本质是告诉 webpack:

  • 如果你遇到了至少一处用到 axios 变量的模块实例,那请你将 axios package 引入进来,并将其提供给需要用到它的模块。

注意: 在axios的源码中导入的方式发生了改变:

image-20240903161849056

提取CSS

  • new MiniCssExtractPlugin()({filename, chunkFilename}),用于将 CSS 提取到单独的文件中,而不是将其嵌入到 JavaScript 文件中。
    • filenamestring,指定生成的 CSS 文件名。可以使用占位符 [name][id][contenthash] 等。
    • chunkFilenamestring,指定异步加载的 CSS 文件名。
    • 注意: 确保项目中没有同时使用 style-loaderMiniCssExtractPlugin.loader,否则可能会出现冲突。

依赖包:

  • mini-css-extract-plugin:帮助我们将 css 提取到一个独立的 css 文件中,该插件需要在 webpack4+才可以使用。
    • 安装:npm install mini-css-extract-plugin -D

配置:

配置 rules 和 plugins:

image-20240229141650610

注意:

  • 确保项目中没有同时使用 style-loaderMiniCssExtractPlugin.loader,否则可能会出现冲突。

  • 建议开发阶段使用style-loader,生产阶段使用MiniCssExtractPlugin.loader

Hash

在我们给打包的文件进行命名的时候,会使用 placeholder,placeholder 中有几个属性比较相似:hash、chunkhash、contenthash

  • hash 本身是通过 MD4 的散列函数处理后,生成一个 128 位的 hash 值(32 个十六进制);

  • hash 值的生成和整个项目有关系:

    • 比如我们现在有两个入口 index.js 和 main.js
    • 它们分别会输出到不同的 bundle 文件中,并且在文件名称中我们有使用 hash;
    • 这个时候,如果修改了 index.js 文件中的内容,那么main.js和index.js的 hash 都会发生变化
    • 那就意味着两个文件的名称都会发生变化;
  • chunkhash 可以有效的解决上面的问题,它会根据不同的入口来生成 hash 值:

    • 比如我们修改了 index.js,那么 main.js 的 chunkhash 是不会发生改变的;
  • contenthash (推荐),表示生成的文件 hash 名称,只和内容有关系:

    • 比如我们的 index.js,引入了一个 style.css,style.css 有被抽取到一个独立的 css 文件中;
    • 这个 css 文件在命名时,如果我们使用的是 chunkhash;
    • 那么当 index.js 文件的内容发生变化时,css 文件的命名也会发生变化;
    • 这个时候我们可以使用 contenthash,css 文件名不变
    • 注意: 如果是通过 import()函数异步加载的话contenthash也会改变

DLL

概念:

  • DLL 全程是动态链接库(Dynamic Link Library),是为软件在 Windows 中实现共享函数库的一种实现方式

  • 那么 webpack 中也有内置 DLL 的功能,它指的是我们可以将能够共享,并且不经常改变的代码,抽取成一个共享的库;

  • 这个库在之后编译的过程中,会被引入到其他项目的代码中;

基本使用 :

DLL 库的使用分为两步:

  • 第一步:打包一个 DLL 库;

  • 第二步:项目中引入 DLL 库;

注意: 在升级到 webpack4 之后,React 和 Vue 脚手架都移除了 DLL 库(下面的 vue 作者的回复),所以知道有这么一个概念即可。

image-20240229141720325

压缩-JS、CSS

JS压缩

Terser介绍和安装

依赖包:

  • terser:用于压缩混淆JS代码的工具
    • 安装:npm install terser -D

概念:

  • Terser 是一个 JavaScript 的解释(Parser)、Mangler(绞肉机/混淆)/Compressor(压缩机)的工具集

  • Terser 是一个用于压缩混淆JS代码的工具,它通常被用于与 Webpack 或其它构建工具一起使用,以减小前端资源文件的体积提高加载性能

  • 早期我们会使用 uglify-js 来压缩、丑化我们的 JavaScript 代码,但是目前已经不再维护,并且不支持 ES6+的语法;

  • Terser 是从 uglify-es fork 过来的,并且保留它原来的大部分 API 以及适配 uglify-esuglify-js@3 等;

也就是说,Terser 可以帮助我们压缩、丑化我们的代码,让我们的 bundle 变得更小

安装:

因为 Terser 是一个独立的工具,所以它可以单独安装:

sh
# 全局安装
npm install terser -g

# 局部安装
npm install terser -D
命令行使用

我们可以在命令行中使用 Terser:

sh
terser [input files] [options]

# 举例说明
terser js/file1.js -o foo.min.js -c -m

npx terser ./src/abc.js 
  -o abc.min.js 
  -c arrows,arguments=true,dead_code 
  -m toplevel=true,keep_classnames=true,keep_fnames=true

options选项:

  • js/file1.js:要压缩的输入JS文件。

  • -o foo.min.js:指定输出的压缩后的JS文件。

  • -c:Compress,启用压缩选项,开启后会对代码进行压缩优化,包括删除不必要的代码、简化表达式等。

    • arrowsboolean,(默认false),将class或者object中的函数转换成箭头函数
    • argumentsboolean,(默认false),将函数中使用的arguments[index]转成对应的形参名称
    • dead_codeboolean,(默认false),移除不可达的代码(tree shaking)
    • 其他属性:https://github.com/terser/terser#compress-options

    image-20240311103312027

  • -m:Mangle,启用混淆选项(变量名混淆),开启后会对变量名进行短名称替换,减小代码体积并提高执行效率。

    • toplevelboolean,(默认false),对顶层作用域中的变量名称进行丑化
    • keep_classnamesboolean,(默认false),是否保持依赖的类名称
    • keep_fnamesboolean,(默认false),是否保持原来的函数名称
    • 其他属性:https://github.com/terser/terser#mangle-options

    image-20240311103644458

sh
npx terser ./src/abc.js 
  -o abc.min.js 
  -c arrows,arguments=true,dead_code 
  -m toplevel=true,keep_classnames=true,keep_fnames=true
webpack配置

配置: 在optimization中

  • minimizeboolean,告知webpack使用 TerserPlugin 或其它在 optimization.minimizer 定义的插件压缩 bundle。

  • minimizer[new TerserPlugin()?, new CssMinimizer()?, '...'?]

    ​ 允许你通过提供一个或多个定制过的 TerserPlugin 实例,覆盖默认压缩工具(minimizer)。

    • new TerserPlugin()?( {extractComments?, parallel?, terserOptions?} )

      ​ 用于对JS代码简化的插件

      • 依赖包:terser-webpack-plugin (内置插件)
      • extractCommentsboolean,(D: true),将注释抽取到一个单独的文件中。
        • 注意: 开发中,不希望保留这个注释时,可以设为 false
      • parallelboolean | number,(D: true),使用多进程并发运行提高构建的速度
        • 注意: 并发运行的默认数量: os.cpus().length - 1
      • terserOptions{compress, mangle, toplevel, keep_classnames, keep_fnames},设置terser配置
        • compress{arrows, arguments, dead_code, unused},设置压缩相关的选项
          • arrowsboolean,(),将class或者object中的函数转换成箭头函数
          • argumentsboolean,(),将函数中使用的arguments[index]转成对应的形参名称
          • dead_codeboolean,(D: true),移除不可达的代码(tree shaking)
          • unusedboolean,(D: true),移除未使用过的函数(tree shaking)
        • mangleboolean,设置丑化相关的选项,可以直接设置为 true
        • toplevelboolean,顶层变量是否进行转换
        • keep_classnamesboolean,保留类的名称
        • keep_fnamesboolean,保留函数的名称
    • new CssMinimizer()?( {parallel?, minimizerOptions?} ),压缩和优化 CSS 代码的插件

      • 依赖包:css-minimizer-webpack-plugin (内置插件)
      • parallel?boolean,(D: true),是否开启多进程并行压缩
      • minimizerOptions?{preset},传递给 CssMinimizer压缩器的选项,如指定压缩规则、是否启用并行压缩等
    • '...'?,可以使用 '...' 来访问默认值

真实开发中,我们不需要手动的通过 terser 来处理我们的代码,我们可以直接通过 webpack 来处理:

  • 在 webpack 中有一个 minimizer 属性,在 production 模式下,默认就是使用 TerserPlugin 来处理我们的代码的;

  • 如果我们对默认的配置不满意,也可以自己来创建 TerserPlugin 的实例,并且覆盖相关的配置;

自定义minimizer:

1、我们需要打开 minimize,让其对我们的代码即使在development模式下页可以进行压缩(默认 production 模式下已经打开了)

image-20240311105653217

2、我们可以在 minimizer 创建一个 TerserPlugin,并设置配置选项

image-20240311111231192

image-20240904110631724

CSS压缩

  • minimizeboolean,告知webpack使用 TerserPlugin 或其它在 optimization.minimizer 定义的插件压缩 bundle。

  • minimizer[new TerserPlugin()?, new CssMinimizer()?, '...'?]

    ​ 允许你通过提供一个或多个定制过的 TerserPlugin 实例,覆盖默认压缩工具(minimizer)。

    • new TerserPlugin()?( {extractComments?, parallel?, terserOptions?} )

      ​ 用于对JS代码简化的插件

      • 依赖包:terser-webpack-plugin (内置插件)
      • extractCommentsboolean,(D: true),将注释抽取到一个单独的文件中。
        • 注意: 开发中,不希望保留这个注释时,可以设为 false
      • parallelboolean,(D: true),使用多进程并发运行提高构建的速度
        • 注意: 并发运行的默认数量: os.cpus().length - 1
      • terserOptions{compress, mangle, toplevel, keep_classnames, keep_fnames},设置terser配置
        • compress{arrows, arguments, dead_code, unused},设置压缩相关的选项
          • arrowsboolean,(),将class或者object中的函数转换成箭头函数
          • argumentsboolean,(),将函数中使用的arguments[index]转成对应的形参名称
          • dead_codeboolean,(D: true),移除不可达的代码(tree shaking)
          • unusedboolean,(D: true),移除未使用过的函数(tree shaking)
        • mangleboolean,设置丑化相关的选项,可以直接设置为 true
        • toplevelboolean,顶层变量是否进行转换
        • keep_classnamesboolean,保留类的名称
        • keep_fnamesboolean,保留函数的名称
    • new CssMinimizer()?( {parallel?, minimizerOptions?} ),压缩和优化 CSS 代码的插件

      • 依赖包:css-minimizer-webpack-plugin (内置插件)
      • parallel?boolean,(D: true),是否开启多进程并行压缩
      • minimizerOptions?{preset},传递给 CssMinimizer压缩器的选项,如指定压缩规则、是否启用并行压缩等
    • '...'?,可以使用 '...' 来访问默认值

依赖包:

  • css-minimizer-webpack-plugin
    • 安装:npm i css-minimizer-webpack-plugin -D

另一个代码的压缩是 CSS:

  • CSS 压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等;

  • CSS 的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin

  • css-minimizer-webpack-plugin 是使用 cssnano 工具来优化、压缩 CSS(也可以单独使用);

1、安装 css-minimizer-webpack-plugin:

sh
npm install css-minimizer-webpack-plugin -D

2、在 optimization.minimizer 中配置

image-20240229142336192

image-20240904112408307

抽取Webpack配置

依赖包:

  • webpack-merge
    • 安装:pnpm add webpack-merge -D

抽取思路:

  • 1、将配置文件导出一个函数,而不是一个对象
  • 2、从上向下查看所有的配置属性应该属于哪一个文件:comm/dev/prod
  • 3、针对单独的配置文件进行定制化。如css加载:使用不同的loader可以根据isProduction动态获取

1、在pakage.json中指定打包和服务器的配置文件

image-20240311114115387

2、修改comm.config.js中的打包目录

image-20240311114306847

3、补充知识: webpack允许导出一个函数

image-20240311114617310

4、在打包时添加--env参数,区分开发、生产环境,该参数可以在config函数中获取到

image-20240311114745678

image-20240311114945967

image-20240311114919043

image-20240311115250207

5、通过--env xxx区分开发、生产环境,返回不同的配置文件

image-20240311120831496

6、创建3个配置文件

image-20240311115347013

7、comm.config.js

8、prod.config.js

9、dev.config.js

10、自动区分style-loaderMiniCssExtractPlugin.loader

1)、将comm配置对象,通过getCommConfig()函数返回,并传入区分环境的参数isProduction

image-20240311121535684

2)、在匹配.css规则处,使用isProduction分别加载不同的loader

image-20240311121805465

Tree Shaking

概述

什么是 Tree Shaking

  • Tree Shaking 是一个术语,在计算机中表示消除死代码(dead_code);

  • 最早的想法起源于 LISP,用于消除未调用的代码纯函数无副作用,可以放心的消除,这也是为什么要求我们在进行函数式编程时,尽量使用纯函数的原因之一);

  • 后来 Tree Shaking 也被应用于其他的语言,比如 JavaScriptDart

JavaScript中的Tree Shaking:

  • 对 JavaScript 进行 Tree Shaking 是源自打包工具 rollup(后面我们也会讲的构建工具);

  • 这是因为 Tree Shaking 依赖于 ES Module 的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系);

  • webpack2 正式内置支持了 ES2015 模块,和检测未使用模块的能力;

  • webpack4 正式扩展了这个能力,并且通过 package.json 的 sideEffects 属性作为标记,告知 webpack 在编译时,哪里文件可以安全的删除掉;

  • webpack5 中,也提供了对部分 CommonJS 的 tree shaking 的支持;

JS实现TS

事实上 webpack 实现 Tree Shaking 采用了两种不同的方案

  • usedExports:通过标记某些函数是否被使用,之后通过 Terser 来进行优化的,决定能否删除函数

  • sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用,决定能否删除文件/模块

image-20240229142401231

usedExports

mode 设置为 development 模式:

  • 为了可以看到 usedExports 带来的效果,我们需要设置为 development 模式

  • 因为在 production 模式下,webpack 默认的一些优化会带来很大的影响。

设置 usedExports 为 true 和 false 对比打包后的代码:

  • 在 usedExports 设置为 true 时,会有一段注释:unused harmony export mul

  • 这段注释的意义是什么呢?告知 Terser 在优化时,可以删除掉这段代码;

这个时候,我们将 minimize 设置 true

  • usedExports 设置为 false 时,mul 函数没有被移除掉;

  • usedExports 设置为 true 时,mul 函数有被移除掉;

所以,usedExports 实现 Tree Shaking 是结合 Terser 来完成的

  • usedExports:用来标记函数是否被使用
  • Terser:用来执行删除未使用的函数

webpack设置:

js
optimization: {
    // production模式下默认开启usedExports
    mode: 'development',
        
    // 不显示source-map
    devtools: false,
        
    // 标记函数是否被使用
+    usedExports: true,
    
    // 通过TerserPlugin删除未使用函数
    minimize: true,
    minimizer: [
      new TerserPlugin({
        compress: {
          unused: true
        }
      })
    ]
}

打包效果(未设置TerserPlugin):

image-20240311161719152

sideEffects

sideEffects 用于告知 webpack compiler 哪些模块是有副作用的:

  • 副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过 export 来判断这段代码的意义;

  • 副作用的问题,在讲 React 的纯函数时是有讲过的;

在 package.json 中设置 sideEffects 的值:

  • 如果我们将 sideEffects 设置为 false,就是告知 webpack 可以安全的删除未用到的 exports

  • 如果有一些我们希望保留,可以设置为数组;

比如我们有一个 format.js、style.css 文件:

  • 该文件在导入时没有使用任何的变量来接受;

  • 那么打包后的文件,不会保留 format.js、style.css 相关的任何代码;

1、要被优化的模块parse-lyric.js

  • 如果该模块中存在副作用代码,设置了sideEffects: false后,该副作用代码也会被删除
  • 推荐: 平时编写模块的时候,尽量编写纯模块

image-20240311163832125

2、在 package.json 中设置 sideEffects为false

image-20240311163620959

3、如果有希望保留的JS文件,可以设置sideEffects为数组

image-20240311164352364

4、CSS文件,需要保留下来

image-20240311164535269

5、打包效果:

image-20240311163710794

image-20240904124126211

最佳方案

所以,如何在项目中对 JavaScript 的代码进行 TreeShaking 呢(生产环境)?

  • 在 optimization 中配置 usedExports 为 true,来帮助 Terser 进行优化;

  • 在 package.json 中配置 sideEffects 为 false,直接对模块进行优化;

CSS实现TS

purgecss-webpack-plugin

依赖包:

  • purgecss-webpack-plugin
    • 安装:npm install purgecss-webpack-plugin -D

上面我们学习的都是关于 JavaScript 的 Tree Shaking,那么 CSS 是否也可以进行 Tree Shaking 操作呢?

  • CSS 的 Tree Shaking 需要借助于一些其他的插件;

  • 在早期的时候,我们会使用 PurifyCss 插件来完成 CSS 的 tree shaking,但是目前该库已经不再维护了(最新更新也是在 4 年前了);

  • 目前我们可以使用另外一个库来完成 CSS 的 Tree Shaking:PurgeCSS,也是一个帮助我们 删除未使用的 CSS 的工具;

安装 PurgeCss 的 webpack 插件:

sh
npm install purgecss-webpack-plugin -D
配置PurgeCss

PurgeCSSPlugin()({paths, safelist?}),检查项目中实际使用的 CSS,并删除未使用的样式。

  • pathsarray,指定要扫描的文件路径。可以使用 glob 模块来匹配路径,确保所有使用 CSS 的文件都被检查。
  • safelist?array,指定一个列表,保留某些类名,即使它们没有被使用。

依赖包:

  • glob
    • 安装:pnpm add glob -D

配置插件(生产环境):

  • paths:表示要检测哪些目录下的内容需要被分析,这里我们可以使用 glob;

  • 默认情况下,Purgecss 会将我们的 html 标签的样式移除掉,如果我们希望保留,可以添加一个 safelist 的属性;

image-20240311165904311

image-20240311173152731

purgecss 也可以对 less 文件进行处理(因为它是对打包后的 css 进行 tree shaking 操作);

问题: Purgecss打包后删除了所有的class

原因:

  • 通过glob.sync("${resolve(__dirname, '../src')}/**/*")搜索的文件是一个空数组[]
  • 这是因为glob@8.x版本在windows上会存在兼容性问题,需要降版本到glob@7.x解决

Scope Hoisting

  • new webpack.optimize.ModuleConcatenationPlugin()(),作用域提升。将多个模块的代码合并成一个函数,这样可以减少函数调用的开销,提高执行效率。

概述:

什么是 Scope Hoisting 呢?

  • Scope Hoisting 从 webpack3 开始增加的一个新功能;

  • 功能是对作用域进行提升,并且让 webpack 打包后的代码更小、运行更快

默认情况下 webpack 打包会有很多的函数作用域,包括一些(比如最外层的)IIFE:

  • 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;

  • Scope Hoisting 可以将函数合并到一个模块中来运行;

配置:

使用 Scope Hoisting 非常的简单,webpack 已经内置了对应的模块:

  • production 模式下,默认这个模块就会启用

  • development 模式下,我们需要自己打开该模块;

    image-20240311173948499

    image-20240311174031417

打包效果:将多个模块打包到一个模块中

image-20240311174207687

压缩-HTTP

概述

HTTP 压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式

HTTP 压缩的流程什么呢?

  • 第一步:HTTP 数据在服务器发送前就已经被压缩了;(可以在 webpack 中完成)

  • 第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式;

    image-20240229142525419

  • 第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器;

    image-20240229142536263

压缩格式

目前的压缩格式非常的多:

  • compress – UNIX 的“compress”程序的方法(历史性原因,不推荐大多数应用使用,应该使用 gzip 或 deflate);

  • deflate – 基于deflate算法(定义于 RFC 1951)的压缩,使用 zlib 数据格式封装;

  • gzip – GNU zip 格式(定义于 RFC 1952),是目前使用比较广泛的压缩算法;

  • br – 一种新的开源压缩算法,专为 HTTP 内容的编码而设计;

Webpack对文件压缩

webpack 中相当于是实现了 HTTP 压缩的第一步操作,我们可以使用 CompressionPlugin

依赖包:

  • compression-webpack-plugin
    • 安装:npm install compression-webpack-plugin -D

配置:

  • new CompressionPlugin():,文件压缩插件
    • ( {test?, minRatio?, algorithm?, threshold?, include?, exclude?} )
    • test?reg | string | array,(D: 所有文件),匹配哪些文件需要压缩
    • minRatio?number,(D: 0.8),至少的压缩比例
    • algorithm?'gzip' | 'brotli',(D: 'gzip'),采用的压缩算法
    • threshold?number,(D: 0),设置文件从多大开始压缩
    • include?reg | string | array,(D: 所有文件),指定要包含的文件路径
    • exclude?reg | string | array,(D: null),指定要排除的文件路径

image-20240229142557388

压缩-HTML

我们之前使用了 HtmlWebpackPlugin 插件来生成 HTML 的模板,事实上它还有一些其他的配置:

  • new HtmlWebpackPlugin()( {title?, template?, inject?, cache?, minify?} ),快速创建 HTML 模板用于打包
    • 基本选项
    • title?string,(D: 'Webpack App'),指定生成的 HTML 文件的标题
    • template?string,(D: ''),指定用作模板的 HTML 文件路径
    • HTML压缩
    • inject?true | false | 'body' | 'head',(D: true),指定打包的 bundle 文件插入的位置
    • cache?boolean,(D: true),控制是否应该缓存生成的结果以提高构建性能
    • minify?boolean | object,(D: 生产:true, 其他:false),是否压缩 HTML 文件。会用到 html-minifier-terser 插件
      • object选项
      • collapseWhitespace: (D: true),去除空格
      • removeComments: (D: true),删除注释
      • removeEmptyAttributes: (D: true),删除空属性
      • removeRedundantAttributes: (D: true),删除多余的属性
      • removeScriptTypeAttributes: (D: true),删除 script 标签的 type 属性
      • removeStyleLinkTypeAttributes: (D: true),删除 style 和 link 标签的 type 属性
      • useShortDoctype: (D: true),使用短的 doctype 声明
      • minifyJS: 压缩内嵌的 JavaScript 代码
      • minifyCSS: 压缩内嵌的 CSS 代码

image-20240229142613055

打包分析

分析一:打包的时间分析

依赖包:

  • speed-measure-webpack-plugin
    • 安装:npm install speed-measure-webpack-plugin -D

如果我们希望看到每一个 loader、每一个 Plugin 消耗的打包时间,可以借助于一个插件:speed-measure-webpack-plugin

  • 注意: 该插件在最新的 webpack 版本中存在一些兼容性的问题(和部分 Plugin 不兼容: MiniCssExtractPlugin

  • 截止 2021-3-10 日,但是目前该插件还在维护,所以可以等待后续是否更新;

  • 我这里暂时的做法是把不兼容的插件先删除掉,也就是不兼容的插件不显示它的打包时间就可以了;

第一步,安装 speed-measure-webpack-plugin 插件

sh
npm install speed-measure-webpack-plugin -D

第二步,使用 speed-measure-webpack-plugin 插件

  • 创建插件导出的对象 SpeedMeasurePlugin;
  • 使用 smp.wrap 包裹我们导出的 webpack 配置;

测量plugins:

image-20240229142646324

测量整个配置:

image-20240312121401290

测量分析:

image-20240312121923907

改善loader速度:添加exclude:/\\/node_modules\\//

image-20240312122123897

分析二:打包后文件分析

方案一: 生成一个 stats.json 的文件

在打包命令后面添加:--profile --json=stats.json参数

sh
"build:stats": "webpack --config ./config/webpack.common.js --env production --profile --json=stats.json",

通过执行 npm run build:status 可以获取到一个 stats.json 的文件:

image-20240229142711543

方案二: 使用 webpack-bundle-analyzer 工具

另一个非常直观查看包大小的工具是 webpack-bundle-analyzer。

第一步,我们可以直接安装这个工具:

sh
npm install webpack-bundle-analyzer -D

第二步,我们可以在 webpack 配置中使用该插件:

image-20240229142731116

在打包 webpack 的时候,这个工具是帮助我们打开一个 8888 端口上的服务,我们可以直接的看到每个包的大小。

  • 比如有一个包是通过一个 Vue 组件打包的,但是非常的大,那么我们可以考虑是否可以拆分出多个组件,并且对其进行懒加载;

  • 比如一个图片或者字体文件特别大,是否可以对其进行压缩或者其他的优化处理;

打包效果:

image-20240312135702258