Webpack通识

Webpack相对其他打包器不同的地方

Webpack忽略具体资源类型之间的差异,将所有代码/非代码文件都统一看作Module------模块对象,以相同的加载、解析、依赖管理、优化、合并流程实现打包,并借助Loader、Plugin两种开放接口将资源差异处理逻辑转交由社区实现,实现统一资源构建模型

优点:

  • 所有资源都是Module,所以可以用同一套代码实现诸多特性,包括:代码压缩、Hot Module Replacement、缓存等

  • 打包时,资源与资源之间非常容易实现信息互换,例如可以轻易在HTML插入Base64格式的图片

  • 借助Loader,Webpack几乎可以用任意方式处理任意类型的资源,例如可以用Less、Stylus、Sass等预编译CSS代码

Webpack的打包过程

  • 输入:从文件系统读入代码文件

  • 模块递归处理:调用Loader转译Module内容,并将结果转换为AST,从中分析出模块依赖关系,进一步递归调用模块处理过程,直到所有依赖文件都处理完毕

  • 后处理:所有模块递归处理完毕后开始执行后处理,包括模块合并、注入运行时、产物优化等,最终输出Chunk集合

  • 输出:将Chunk写出到外部文件系统

Webpack配置项分类

  • 流程类:作用于打包流程某个或若干个环节,直接影响编译打包效果的配置项
  • 工具类:打包主流程之外,提供更多工程化工具的配置项

流程类

  • 输入输出:

    • entry:用于定义项目入口文件,Webpack会从这些入口文件开始按图索骥找出所有项目文件
    • context:项目执行上下文路径
    • output:配置产物输出路径、名称等
  • 模块处理:

    • resolve:用于配置模块路径解析规则,可用于帮助Webpack更精准、高效地找出指定模块
    • module:用于配置模块加载规则,例如针对什么类型的资源需要使用哪些Loader进行处理
    • externals:用于声明外部资源,Webpack会直接忽略这部分资源,跳出这些资源的解析、打包操作
  • 后处理

    • optimization:用于控制如何优化产物包体积,内置Dead Code Elimination、Scope Hoisting、代码混淆、代码压缩等功能
    • target:用于配置编译产物的目标运行环境,支持web、node、electron等值,不同值最终产物会有所差异
    • mode:编译模式短语,支持developmentproduction等值,可以理解为一种声明环境的短语

工具类

  • 开发效率类:

    • watch:用于配置持续监听文件变化,持续构建
    • devtool:用于配置产物Sourcemap生成规则
    • devServer:用于配置与HMR强相关的开发服务器功能
  • 性能优化类:

    • cache:Webpack5之后,该项用于控制如何缓存编译过程信息与编译结果
    • performance:用于配置当产物大小超过阈值时,如何通知开发者
  • 日志类:

    • state:用于精准地控制编译过程的日志内容,在做比较细致的性能调试时非常有用
    • infrastructureLogging:用于控制日志输出方式,例如可以通过该配置将日志输出到磁盘文件

Webpack常用Loader、Plugin

Webpack打包npm包

  1. 相对于普通项目需要在output#library下加name和type
java 复制代码
module.exports = {
  // ...
  output: {
    filename: "[name].js",
    path: path.join(__dirname, "./dist"),
+   library: {
+     name: "_",  // 被调用时的名字
+     type: "umd",
+   },
  },
  // ...
};
  1. 排除第三方库 防止打包体积过大 使用externals
java 复制代码
// webpack.config.js
module.exports = {
  // ...
+  externals: {
+   lodash: {
+     commonjs: "lodash",
+     commonjs2: "lodash",
+     amd: "lodash",
+     root: "_",
+   },
+ },
  // ...
};
  1. 不再打包lodash可以顺手将lodash声明为peerDependencies
json 复制代码
{
  "name": "6-1_test-lib",
  // ...
+ "peerDependencies": {
+   "lodash": "^4.17.21"
+ }
}
  1. 也可以用webpack-node-externals直接排除所有node_modules模块
ini 复制代码
// webpack.config.js
const nodeExternals = require('webpack-node-externals');

module.exports = {
  // ...
+  externals: [nodeExternals()]
  // ...
};
  1. 抽离CSS代码,通常需要使用mini-css-extract-plugin插件将样式抽离成单独文件,由用户自行引入
diff 复制代码
module.exports = {  
  // ...
+ module: {
+   rules: [
+     {
+       test: /.css$/,
+       use: [MiniCssExtractPlugin.loader, "css-loader"],
+     },
+   ],
+ },
+ plugins: [new MiniCssExtractPlugin()],
};
  1. 生成sourcemap
java 复制代码
// webpack.config.js
module.exports = {  
  // ...
+ devtool: 'source-map'
};
  1. 使用.npmignore文件忽略不需要发布到npm的文件
  2. package.json文件中,使用prepublishOnly指令,在发布前自动执行编译命令,例如
json 复制代码
// package.json
{
  "name": "test-lib",
  // ...
  "scripts": {
    "prepublishOnly": "webpack --mode=production"
  },
  // ...
}
  1. package.json文件中,使用main指定项目入口,同时使用modue指定ES Module模式下的入口,以允许用户直接使用源码版本,例如:
json 复制代码
{
  "name": "6-1_test-lib",
  // ...
  "main": "dist/main.js",
  "module": "src/index.js",
  "scripts": {
    "prepublishOnly": "webpack --mode=production"
  },
  // ...
}

Webpack处理图像

导入图像

  1. file-loader 已经内置不需要安装
java 复制代码
// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [{
      test: /.(png|jpg)$/,
-     use: ['file-loader']
+     type: 'asset/resource'
    }],
  },
};
  1. url-loader 限定文件大小阈值
yaml 复制代码
module.exports = {
  // ...
  module: {
    rules: [{
      test: /.(png|jpg)$/,
-     use: [{
-       loader: 'url-loader',
-       options: {
-         limit: 1024
-       }
-     }]
+     type: "asset",
+     parser: {
+        dataUrlCondition: {
+          maxSize: 1024 // 1kb
+        }
+     }
    }],
  },
};
  1. raw-loader
bash 复制代码
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.svg$/i,
-       use: ['raw-loader']
+       type: "asset/source"
      },
    ],
  },
};

图像优化

压缩

  1. image-webpack-loader
arduino 复制代码
yarn add -D image-webpack-loader

module.exports = {
  // ...
  module: {
    rules: [{
      test: /.(gif|png|jpe?g|svg)$/i,
      // type 属性适用于 Webpack5,旧版本可使用 file-loader
      type: "asset/resource",
      use: [{
        loader: 'image-webpack-loader',
        options: {
          // jpeg 压缩配置
          mozjpeg: {
            quality: 80
          },
          disable: process.env.NODE_ENV === 'development' // 非常消耗性能,建议只在生产环境下使用
        }
      }]
    }],
  },
};

image-webpack-loader底层依赖于imagemin及一系列的图像优化工具:

  • mozjpeg:用于压缩JPG(JPEG)图片
  • optipng:用于压缩PNG图片
  • pngquant:用于压缩PNG图片
  • svgo:用于压缩SVG图片
  • gifsicle:用于压缩Gif图
  • webp:用于将JPG/PNG图片压缩并转化为WebP图片格式

雪碧图

  1. 使用webpack-spritesmith自动实现雪碧图效果
css 复制代码
yarn add -D webpack-spritesmith

module.exports = {
  // ...
  resolve: {
    modules: ["node_modules", "assets"]
  },
  plugins: [
    new SpritesmithPlugin({
      // 需要
      src: {
        cwd: path.resolve(__dirname, 'src/icons'),
        glob: '*.png'
      },
      target: {
        image: path.resolve(__dirname, 'src/assets/sprite.png'),
        css: path.resolve(__dirname, 'src/assets/sprite.less')
      }
    })
  ]
};

webpack-spritesmith会将src.cwd目录内所有匹配src.glob规则的图片合并成一张大图并保存到target.image指定的文件路径

响应式图片

  1. 使用responsive-loader
css 复制代码
yarn add -D responsive-loader sharp

module.exports = {
  // ...
  module: {
    rules: [{
      test: /.(png|jpg)$/,
      oneOf: [{
        type: "javascript/auto",
        resourceQuery: /sizes?/,
        use: [{
          loader: "responsive-loader",
          options: {
            adapter: require("responsive-loader/sharp"),
          },
        }],
      }, {
        type: "asset/resource",
      }],
    }],
  }
};
  1. 注意,实践中我们通常没必要对项目里所有图片都施加响应式特性,因此这里使用resourceQuery过滤出带size/sizes参数的图片引用,使用方法:
ini 复制代码
// 引用图片,并设置响应式参数
import responsiveImage from './webpack.jpg?sizes[]=300,sizes[]=600,sizes[]=1024';

const Picture = function () {
  return (
    <img
      srcSet={responsiveImage.srcSet}
      src={responsiveImage.src}
      sizes="(min-width: 1024px) 1024px, 100vw"
      loading="lazy"
    />
  );
};

上例的引用参数./webpack.jpg?sizes[]=300,sizes[]=600,sizes[]=1024最终将生成宽度分别为300、600、1024三张图片,之后设置img标签的srcset属性即可实现图片响应式功能

css 复制代码
.foo {
    background: url("./webpack.jpg?size=1024");
}

@media (max-width: 480px) {
    .foo {
        background: url("./webpack.jpg?size=300");
    }
}

Webpack环境治理

  1. 实现同一份代码打包出多种产物,使用数组方式配置
lua 复制代码
// webpack.config.js
module.exports = [
  {
    output: {
      filename: './dist-amd.js',
      libraryTarget: 'amd',
    },
    name: 'amd',
    entry: './app.js',
    mode: 'production',
  },
  {
    output: {
      filename: './dist-commonjs.js',
      libraryTarget: 'commonjs',
    },
    name: 'commonjs',
    entry: './app.js',
    mode: 'production',
  },
];
  1. 可以借助webpack-merge合并通用配置
php 复制代码
const { merge } = require("webpack-merge");

const baseConfig = {
  output: {
    path: "./dist"
  },
  name: "amd",
  entry: "./app.js",
  mode: "production",
};

module.exports = [
  merge(baseConfig, {
    output: {
      filename: "[name]-amd.js",
      libraryTarget: "amd",
    },
  }),
  merge(baseConfig, {
    output: {
      filename: "./[name]-commonjs.js",
      libraryTarget: "commonjs",
    },
  }),
];
  1. 使用配置函数的方式配置webpack,允许用户根据命令行参数动态创建配置对象,可用于实现简单的多环境治理策略
css 复制代码
// npx webpack --env app.type=miniapp --mode=production
module.exports = function (env, argv) {
  return {
    mode: argv.mode ? "production" : "development",
    devtool: argv.mode ? "source-map" : "eval",
    output: {
      path: path.join(__dirname, `./dist/${env.app.type}`,
      filename: '[name].js'
    },
    plugins: [
      new TerserPlugin({
        terserOptions: {
          compress: argv.mode === "production", 
        },
      }),
    ],
  };
};
命令: env 参数值:
npx webpack --env prod { prod: true }
npx webpack --env prod --env min { prod: true, min: true }
npx webpack --env platform=app --env production { platform: "app", production: true }
npx webpack --env foo=bar=app { foo: "bar=app"}
npx webpack --env app.platform="staging" --env app.name="test" { app: { platform: "staging", name: "test" }

环境治理策略

需求:

  • 开发环境需要使用webpack-dev-server实现Hot Module Replacement
  • 测试环境需要带上完整的Sourcemap内容,以帮助更好地定位问题
  • 生产环境需要尽可能打包出更快、更小、更好的应用代码,确保用户体验

方案

  • 上面使用到的配置函数配合命令行参数动态计算配置对象
  • 业界比较流行的是将不同环境配置分别维护在单独的配置文件中 之后配合--config选项指定配置目标
arduino 复制代码
npx webpack --config webpack.development.js
arduino 复制代码
.
└── config
  ├── webpack.common.js
  ├── webpack.development.js
  ├── webpack.testing.js
  └── webpack.production.js

Webpack入口Entry

使用 entry.dependOn 声明入口依赖:

css 复制代码
module.exports = {
  // ...
  entry: {
    main: "./src/index.js",
    foo: { import: "./src/foo.js", dependOn: "main" },
  },
};

foo入口的dependOn属性指向main入口,此时Webpack认为:客户端在加载foo产物之前必然会加载main,因此可以将重复的模块代码、运行时代码等都放到main产物,减少不必要的重复

dependOn适用于哪些有明确入口依赖的场景,例如我们构建了一个主框架Bundle,其中包含了项目基本框架(如React),之后还需要为每个页面单独构建Bundle,这些页面代码也都依赖于主框架代码,此时可用dependOn属性优化产物内容,减少代码重复

使用 entry.runtime 管理运行时代码:

css 复制代码
const path = require("path");

module.exports = {
  mode: "development",
  devtool: false,
  entry: {
    main: { import: "./src/index.js", runtime: "common-runtime" },
    foo: { import: "./src/foo.js", runtime: "common-runtime" },
  },
  output: {
    clean: true,
    filename: "[name].js",
    path: path.resolve(__dirname, "dist"),
  },
};

为支持产物代码在各种环境中正常运行,Webpack会在产物文件中注入一系列运行时代码,用以支撑起整个应用框架,运行时代码的多寡取决于我们用到多少特性,例如:

  • 需要导入导出文件时,将注入__webpack_require_.r
  • 使用异步加载时,将注入__webpack_require_.l

不要小看运行时代码量,极端情况下甚至可能超过业务代码总量,为此,必要时我们可以尝试使用runtime配置将运行时抽离为单独Bundle,如上:

实例中,mainfoo入口均将runtime声明为common-runtime,此时Webpack会将两个入口的运行时代码都抽取出来,放在common-runtimeBundle中

entry.runtime是一种常用的应用性能优化手段

Webpack 出口 Output

  • target支持设置构建目标

    • web
    • browserslist
    • electron
php 复制代码
const path = require("path");
const { merge } = require("webpack-merge");

const baseConfig = {
  mode: "development",
  target: "web",
  devtool: false,
  entry: {
    main: { import: "./src/index.js" },
  },
  output: {
    clean: true,
    path: path.resolve(__dirname, "dist"),
  },
};

module.exports = [
  merge(baseConfig, { target: "web", output: { filename: "web-[name].js" } }),
  merge(baseConfig, { target: "node", output: { filename: "node-[name].js" } }),
];

性能优化

Webpack底层工作流程

  1. 初始化阶段:

    1. 初始化参数:从配置文件、配置对象、Shell参数中读取,与默认配置结合得出最终的参数
    2. 创建编译器对象:用上一步的到的参数创建Compiler对象
    3. 初始化编译环境:包括注入内容插件、注册各种模块工厂、初始化RuleSet集合、加载配置的插件等
    4. 开始编译:执行comiler对象的run方法,创建Compilation对象
    5. 确定入口:根据配置中的entry找出所有的入口文件,调用compilation.addEntry将入口文件转换为dependence对象
  2. 构建阶段:

    1. 编译模块:从entry文件开始,调用loader将模块转译为标准JS内容,调用JS解析器将内容转换为AST对象,从中找出该模块依赖的模块,再递归处理这些依赖模块,直到所有入口依赖的文件都经过了本步骤的处理
    2. 完成模块编译:上一步递归处理所有能触达到的模块后,的到了每个模块被翻译后的内容以及它们之间的依赖关系图
  3. 封装阶段:

    1. 合并:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk
    2. 优化:对上述Chunk施加一系列优化操作,包括:tree-shakingterserscope-hoisting压缩Code Split
    3. 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

可能造成性能问题的地方

  • 构建阶段:

    • 首先需要将文件的相对引用路径转换为绝对路径,这个过程可能涉及多次IO操作,执行效率取决于文件层次深度
    • 找到具体文件后,需要读入文件内容并调用loader-runner遍历Loader数组完成内容转译,这个过程需要执行较密集的CPU操作,执行效率取决于Loader的数量与复杂度
    • 需要将模块内容解析为AST结构,并遍历AST找出模块的依赖资源,这个过程同样需要较密集的CPU操作,执行效率取决于代码复杂度
    • 递归处理依赖资源,执行效率取决于模块数量
  • 封装阶段:

    • 根据splitChunks配置、entry配置、动态模块引用语句等,确定模块与Chunk的映射关系,其中splitChunks相关的分包算法非常复杂,涉及大量CPU计算
    • 根据optimization配置执行一系列产物优化操作,特别是Terser插件需要执行大量AST相关的运算,执行效率取决于产物代码量

性能分析

Webpack内置了stats接口,专门用于统计模块构建耗时,模块依赖关系等信息,推荐用法

  1. 添加profile=true配置
java 复制代码
// webpack.config.js
module.exports = {
  // ...
  profile: true
}
  1. 运行编译命令,并添加--json参数,参数值为最终生成的统计文件名,如:
ini 复制代码
npx webpack --json=stats.json
  1. 最后生成stats.json如下
json 复制代码
{
  "hash": "2c0b66247db00e494ab8",
  "version": "5.36.1",
  "time": 81,
  "builtAt": 1620401092814,
  "publicPath": "",
  "outputPath": "/Users/tecvan/learn-webpack/hello-world/dist",
  "assetsByChunkName": { "main": ["index.js"] },
  "assets": [
    // ...
  ],
  "chunks": [
    // ...
  ],
  "modules": [
    // ...
  ],
  "entrypoints": {
    // ...
  },
  "namedChunkGroups": {
    // ...
  },
  "errors": [
    // ...
  ],
  "errorsCount": 0,
  "warnings": [
    // ...
  ],
  "warningsCount": 0,
  "children": [
    // ...
  ]
}
  • modules:本次打包处理的所有模块列表,内容包含模块的大小、所属chunk、构建原因、依赖模块等,特别是modules.profile属性,包含了构建该模块时,解析路径、编译、打包、子模块打包等各个环节所花费的时间
  • chunks:构建过程生成的chunks列表,数组内容包含chunk名称、大小、包含了哪些模块等
  • assets:编译后最终输出的产物列表、文件路径、文件大小等
  • entrypoints:entry列表,包含动态引入所产生的entry项也会包含在这里面
  • children:子Compiler对象的性能数据,例如extract-css-chunk-plugin插件内部就会调用compilation.createChildCompiler函数创建出子Compiler来做CSS抽取的工作

webpack.js.org/api/stats/

常用性能可视化分析工具

  • Webpack Analysis :Webpack 官方提供的,功能比较全面的 stats 可视化工具;
  • Statoscope:主要侧重于模块与模块、模块与 chunk、chunk 与 chunk 等,实体之间的关系分析;
  • Webpack Visualizer:一个简单的模块体积分析工具,真的很简单!
  • Webpack Bundle Analyzer:应该是使用率最高的性能分析工具之一,主要实现以 Tree Map 方式展示各个模块的体积占比;
  • Webpack Dashboard:能够在编译过程实时展示编译进度、模块分布、产物信息等;
  • Unused Webpack Plugin:能够根据 stats 数据反向查找项目中未被使用的文件。

持久化缓存

仅需在Webpack中设置cache.type='filesystem'

java 复制代码
module.exports = {
    //...
    cache: {
        type: 'filesystem'
    },
    //...
};

cache还提供了若干用于配置缓存效果、缓存周期的配置项,包括

  • type:缓存类型,支持memory|filesystem,需要设置为filesystem才能开启持久缓存
  • cacheDirectory:缓存文件路径,默认为node_modules/.cache/webpack
  • buildDependencies:额外的依赖文件,当这些文件内容发生变化时,缓存会完全失效而执行完整的编译构建,通常可设置为各种配置文件
css 复制代码
module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [
        path.join(__dirname, 'webpack.dll_config.js'),
        path.join(__dirname, '.babelrc')
      ],
    },
  },
};
  • managedPaths:受控目录,Webpack构建时会跳过新旧代码哈希值与时间戳的比较,直接使用缓存副本,默认值为['./node_modules']
  • profile:是否输出缓存处理过程的详细日志,默认为false
  • maxAge:缓存失效时间,默认值为5184000000

并行构建

  • HappyPack:多进程方式运行资源加载(Loader)逻辑;

  • Thread-loader:Webpack 官方出品,同样以多进程方式运行资源加载逻辑;

  • Parallel-Webpack:多进程方式运行多个 Webpack 构建实例;

  • TerserWebpackPlugin:支持多进程方式执行代码压缩、uglify 功能。

Webpack5使用Thread-loader

  1. 安装依赖:
arduino 复制代码
yarn add -D thread-loader
  1. 将 Thread-loader 放在 use 数组首位,确保最先运行,如:
yaml 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        use: [
          {
            loader: "thread-loader",
            options: {
              workers: 2,
              workerParallelJobs: 50,
              // ...
            },
          },
          "babel-loader",
          "eslint-loader",
        ],
      },
    ],
  },
};
  • 在Thread-loader中运行的Loader不能调用emitAsset等接口,这会导致style-loader这一类加载器无法正常工作,解决方案是将这类组件放置在thread-loader之前,如['style-loader','thread-loader','css-loader']

  • Loader中不能活去compilation、compiler等实例对象,也无法活去Webpack配置

并行压缩

Webpack5默认提供Terser来进行代码压缩和混淆

ini 复制代码
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
    optimization: {
        minimize: true,
        minimizer: [new TerserPlugin({
            parallel: 2 // number | boolean
        })],
    },
};

上述配置即可设定最大并行进程数为2

注意:

理论上,并行确实能够提升系统运行效率,但 Node 单线程架构下,所谓的并行计算都只能依托与派生子进程执行,而创建进程这个动作本身就有不小的消耗 ------ 大约 600ms,对于小型项目,构建成本可能可能很低,引入多进程技术反而导致整体成本增加,因此建议大家按实际需求斟酌使用上述多进程方案。

按需编译

使用lazyCompilation,用于实现entry或异步引用模块的按需编译

java 复制代码
// webpack.config.js
module.exports = {
  // ...
  experiments: {
    lazyCompilation: true,
  },
};

启动lazyCompilation后,代码中通过异步引用语句如import('./xxx')导入的模块都不会被立即编译,而是直到页面正式请求该模块资源(例如切换到该路由)时才开始构建,效果与Vite相似,能够极大提升冷启动速度

约束Loader执行范围

使用includeexclude等配置项,限定Loader的执行范围------通常可以排除node_modules文件夹

javascript 复制代码
// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: ["babel-loader", "eslint-loader"],
      },
    ],
  },
};
javascript 复制代码
const path = require("path");
module.exports = {
  // ...
  module: {
    rules: [{
      test: /.js$/,
      exclude: {
        and: [/node_modules/],
        not: [/node_modules/lodash/]
      },
      use: ["babel-loader", "eslint-loader"]
    }],
  }
};
// 支持 and/not/or 

上述,跳过出了loadash之外的

使用noParse跳过已经编译好的文件的编译

java 复制代码
// webpack.config.js
module.exports = {
  //...
  module: {
    noParse: /lodash|react/,
  },
};
// 跳过lodash和react的编译

最好不用,因为跳过了npm,可能导致运行时才能发现一些错误

开发模式仅用产物优化

Webpack提供了许多产物优化功能,但是这些优化在开发环境中意义不大,反而会增加构建器的负担,因此建议关闭这一类优化功能:

  • 确保 mode='development'mode = 'none',关闭默认优化策略;
  • optimization.minimize 保持默认值或 false,关闭代码压缩;
  • optimization.concatenateModules 保持默认值或 false,关闭模块合并;
  • optimization.splitChunks 保持默认值或 false,关闭代码分包;
  • optimization.usedExports 保持默认值或 false,关闭 Tree-shaking 功能;
yaml 复制代码
module.exports = {
  // ...
  mode: "development",
  optimization: {
    removeAvailableModules: false,
    removeEmptyChunks: false,
    splitChunks: false,
    minimize: false,
    concatenateModules: false,
    usedExports: false,
  },
};

最小化watch监控范围

通常情况下,node_modules不会频繁更新,不需要一直监听

javascript 复制代码
// webpack.config.js
module.exports = {
  //...
  watchOptions: {
    ignored: /node_modules/
  },
};

跳过TS类型检查

yaml 复制代码
module.exports = {
  // ...
  module: {
    rules: [{
      test: /.ts$/,
      use: [
        {
          loader: 'ts-loader',
          options: {
            // 设置为"仅编译",关闭类型检查
            transpileOnly: true
          }
        }
      ],
    }],
  }
};

将TS检查交给其他的:

  1. 可以借助编辑器的TypeScript插件实现代码检查
  2. 使用fork-ts-checker-webpack-plugin插件将类型检查能力剥离到子进程执行
javascript 复制代码
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  // ...
  module: {
    rules: [{
      test: /.ts$/,
      use: [
        {
          loader: 'ts-loader',
          options: {
            transpileOnly: true
          }
        }
      ],
    }, ],
  },
  plugins:[
    // fork 出子进程,专门用于执行类型检查
    new ForkTsCheckerWebpackPlugin()
  ]
};

优化ESLint性能

使用新版eslint-webpack-plugin替代旧版eslint-loader

ini 复制代码
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
  // ...
  plugins: [new ESLintPlugin(options)],
  // ...
};

设置resolve缩小搜索范围

  • resolve.extensions :['.js', '.json', '.wasm'] 可以减少匹配的项

SplitChunksPlugin分包

  • minChunks:用于设置引用阈值,被引用次数超过该阈值的 Module 才会进行分包处理;
  • maxInitialRequest/maxAsyncRequests:用于限制 Initial Chunk(或 Async Chunk) 最大并行请求数,本质上是在限制最终产生的分包数量;
  • minSize: 超过这个尺寸的 Chunk 才会正式被分包;
  • maxSize: 超过这个尺寸的 Chunk 会尝试继续做分包;
  • maxAsyncSize: 与 maxSize 功能类似,但只对异步引入的模块生效;
  • maxInitialSize: 与 maxSize 类似,但只对 entry 配置的入口模块生效;
  • enforceSizeThreshold: 超过这个尺寸的 Chunk 会被强制分包,忽略上述其它 size 限制;
  • cacheGroups:用于设置缓存组规则,为不同类型的资源设置更有针对性的分包策略。
yaml 复制代码
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: {
          idHint: "",
          reuseExistingChunk: true,
          minChunks: 2,
          priority: -20
        },
        defaultVendors: {
          idHint: "vendors",
          reuseExistingChunk: true,
          test: /[\/]node_modules[\/]/i,
          priority: -10
        }
      },
    },
  },
};

使用terser-webpack-plugin压缩JS代码

javascript 复制代码
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  entry: { foo: "./src/foo.js", bar: "./src/bar.js" },
  output: {
    filename: "[name].js",
    // ...
  },
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        test: /foo.js$/i,
        extractComments: "all",
      }),
      new TerserPlugin({
        test: /bar.js/,
        extractComments: false,
      }),
    ],
  },
};

terser-webpack-plugin 是一个颇为复杂的 Webpack 插件,提供下述 配置项

  • test:只有命中该配置的产物路径才会执行压缩,功能与 module.rules.test 相似;
  • include:在该范围内的产物才会执行压缩,功能与 module.rules.include 相似;
  • exclude:与 include 相反,不在该范围内的产物才会执行压缩,功能与 module.rules.exclude 相似;
  • parallel:是否启动并行压缩,默认值为 true,此时会按 os.cpus().length - 1 启动若干进程并发执行;
  • minify:用于配置压缩器,支持传入自定义压缩函数,也支持 swc/esbuild/uglifyjs 等值,下面我们再展开讲解;
  • terserOptions:传入 minify ------ "压缩器"函数的配置参数;
  • extractComments:是否将代码中的备注抽取为单独文件,可配合特殊备注如 @license 使用。

使用CssMinimizerWebpackPlugin压缩CSS

javascript 复制代码
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /.css$/,
        // 注意,这里用的是 `MiniCssExtractPlugin.loader` 而不是 `style-loader`
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  optimization: {
    minimize: true,
    minimizer: [
      // Webpack5 之后,约定使用 `'...'` 字面量保留默认 `minimizer` 配置
      "...",
      new CssMinimizerPlugin(),
    ],
  },
  // 需要使用 `mini-css-extract-plugin` 将 CSS 代码抽取为单独文件
  // 才能命中 `css-minimizer-webpack-plugin` 默认的 `test` 规则
  plugins: [new MiniCssExtractPlugin()],
};
  • 使用mini-css-extract-plugin将CSS代码抽取为单独的CSS产物文件,这样才能命中css-minimizer-webpack-plugin默认的test逻辑
  • 使用css-minimizer-webpack-plugin压缩CSS代码

使用HtmlMinifierTerser压缩HTML

csharp 复制代码
yarn add -D html-minimizer-webpack-plugin
xml 复制代码
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");

module.exports = {
  // ...
  optimization: {
    minimize: true,
    minimizer: [
      // Webpack5 之后,约定使用 `'...'` 字面量保留默认 `minimizer` 配置
      "...",
      new HtmlMinimizerPlugin({
        minimizerOptions: {
          // 折叠 Boolean 型属性
          collapseBooleanAttributes: true,
          // 使用精简 `doctype` 定义
          useShortDoctype: true,
          // ...
        },
      }),
    ],
  },
  plugins: [
    // 简单起见,这里我们使用 `html-webpack-plugin` 自动生成 HTML 演示文件
    new HtmlWebpackPlugin({
      templateContent: `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
    <html>
      <head>
        <meta charset="UTF-8" />
        <title>webpack App</title>
      </head>
      <body>
        <input readonly="readonly"/>
        <!-- comments -->
        <script src="index_bundle.js"></script>
      </body>
    </html>`,
    }),
  ],
};

零碎的优化方案

动态加载

dart 复制代码
document.getElementById("someButton").addEventListener("click", async () => {
  // 使用 `import("module")` 动态加载模块
  const someBigMethod = await import("./someBigMethod");
  someBigMethod();
});

HTTP缓存优化 HASH名协商缓存

css 复制代码
module.exports = {
  // ...
  entry: { index: "./src/index.js", foo: "./src/foo.js" },
  output: {
    filename: "[name]-[contenthash].js",
    path: path.resolve(__dirname, "dist"),
  },
  plugins: [new MiniCssExtractPlugin({ filename: "[name]-[contenthash].css" })],
};

使用Tree-Shaking删除多余模块导出

在 Webpack 中,启动 Tree Shaking 功能必须同时满足两个条件:

  • 配置 optimization.usedExportstrue,标记模块导入导出列表;

  • 启动代码优化功能,可以通过如下方式实现:

    • 配置 mode = production
    • 配置 optimization.minimize = true
    • 提供 optimization.minimizer 数组
java 复制代码
// webpack.config.js
module.exports = {
  mode: "production",
  optimization: {
    usedExports: true,
  },
};

使用Scope Hoisting合并模块

Webpack 提供了三种开启 Scope Hoisting 的方法:

  • 使用 mode = 'production' 开启生产模式;
  • 使用 optimization.concatenateModules 配置项;
  • 直接使用 ModuleConcatenationPlugin 插件。
ruby 复制代码
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');

module.exports = {
    // 方法1: 将 `mode` 设置为 production,即可开启
    mode: "production",
    // 方法2: 将 `optimization.concatenateModules` 设置为 true
    optimization: {
        concatenateModules: true,
        usedExports: true,
        providedExports: true,
    },
    // 方法3: 直接使用 `ModuleConcatenationPlugin` 插件
    plugins: [new ModuleConcatenationPlugin()]
};

监控产物体积

javascript 复制代码
module.exports = {
  // ...
  performance: {    
    // 设置所有产物体积阈值
    maxAssetSize: 172 * 1024,
    // 设置 entry 产物体积阈值
    maxEntrypointSize: 244 * 1024,
    // 报错方式,支持 `error` | `warning` | false
    hints: "error",
    // 过滤需要监控的文件类型
    assetFilter: function (assetFilename) {
      return assetFilename.endsWith(".js");
    },
  },
};

Loader

Webpack流程

webpack打包阶段

  1. 初始化阶段:负责设置构建环境,初始化若干工厂类、注入内置插件等
  2. 构建阶段:读入并分析Entry模块,找到模块依赖,之后递归处理这些依赖、依赖的依赖,直到所有模块都处理完毕,这个过程解决资源输入问题
  3. 生成阶段:根据Entry配置将模块封装进不同Chunk对象,经过一系列优化后,再将模块代码翻译为产物形态,按Chunk合并成最终产物文件,这个过程解决资源输出的问题
相关推荐
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte4 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT064 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法