Fwio

Webpack 4 迁移 RFC

  • Start Date: 2022-12-05
  • Target Major Version: v2
  • Reference Issues: none
  • Implemention PR:

Summary

  • 将项目的构建工具从webpack 3迁移至webpack 4,并将各相关 webpack 插件升级到webpack 4兼容版本。

  • 优化构建效率,包括用esbuild代替babel、修改webpack配置、引入插件等,将项目开发环境冷启动时间和生产打包时间缩短约 50%。

  • 尝试将 Vue 2 迁移到vue^2.7.x稳定版本。

    • 由于迁移 Vue 并非构建效率或者基础架构上的优化,所以分支上未提交相关改动

Motivation

构建和 HMR 效率低下

除大页面外,v2 承担了桌面端的绝大部分业务,已经具有相当的复杂度,且随着业务全量迁移到 v2,项目的体积和复杂度会不断增长。

项目庞大的体量导致,在开发阶段npm run dev进行的冷启动时间,以及生产构建npm run build进行的项目打包耗时都会越来越长。

除冷启动速率外,在webpack 3下,项目 HMR (Hot Module Replacement,模块热替换) 的准确率也不尽人意,体现在对某些更改不能准确保留状态,而触发不必要的 live reload。且由于 webpack 在每次 HMR 时都会重新构建整个项目,原本缓慢的构建时间也会拖慢 HMR 的效率。

工具链生态过旧

v2 使用发布于五年前的[email protected]进行构建,部分依赖现已不再有人维护,且vue 2.x在 2023 年年底也将停止维护。应用层框架(vuereact)往往依赖于babelpostcss等编译时工具,而这些工具在实践中又以loaderplugins等形式集成于webpack,因此,未来若要将框架升级,则应提早完成对webpack及底层生态的升级。

另外,v2 项目通过 Vue CLI 生成,其配置自带许多当时通用的 boilerplate,但有些地方已经不是当前的最佳实践(如将所有样式打包成一个.css文件会影响首屏性能等)。在迁移时,通过审阅原有的配置文件,也可优化不合理、移除不必要的部分。

从 vue2.x 到 vue2.7

vue2.7是 Vue 团队为了将 Vue 3 的特性向后兼容,于 2022 年 7 月发布的 Vue 2 最终版本,提供了 Vue 3 最核心的 Composition API 特性与更好的类型系统

兼容 Composition API 和<script setup>语法糖的意义
  • 更好的代码组织,将实现一个业务功能的代码组织在一起,而不是分散到data()computedmethods等多个选项中;
  • 更好的逻辑复用,Composition API 引入的composables提供了比mixin更好的逻辑复用范式;
  • 更好的类型提示,由于 Composition API 使用函数组织组件,相比以往基于 Class 的方式,类型推断更加准确
  • 缩小打包体积,<script setup>被编译为函数,可直接通过作用域访问到渲染上下文,相比于原来通过实例化代理对象的访问更快,且一致的变量名更利于压缩。

Detailed design

  • node.js版本:^14.20.0
  • 由于更新了部分依赖,需先使用npm ci安装依赖

构建工具升级

更新webpack核心依赖

选用发布于两年前的[email protected]作为目标版本。v4.46.0webpack 4的稳定版本,能较好适配各种为webpack 4设计的插件,且由于在4.x中版本较新,能为未来迁移到webpack 5提供便利。

对照发布时间,分别安装和升级 CLI 与 DevServer 依赖到[email protected][email protected]

更新原有webpack工具链

  • Loader:

    • babel-loader^7.1.1 ==> ^8.3.0
      • esbuild替代babel浏览器兼容上的作用,详情见构建效率优化一节。
    • css-loader^0.28.0 ==> ^5.2.7
    • eslint-loader:移除。
      • webpack 4中被废弃,用eslint-webpack-plugin代替。
    • file-loader^1.1.4 ==> ^6.2.0
    • postcss-loader^2.0.8 ==> ^4.3.0
    • url-loader^4.0.8 ==> ^4.1.1
    • vue-loader^13.2.0 ==> ^15.10.1
      • vue-loader 15.xvue-loader目前的最高版本,可为将vue 2.x升级到稳定版本2.7做铺垫。
    • vue-style-loader^3.0.1 ==> ^4.1.3
  • Plugins:

    • copy-webpack-plugin^4.0.1 ==> ^6.4.0
    • eslint-webpack-plugin:新增^2.7.0
      • 用于代替eslint-loader
    • extract-text-webpack-plugin:移除。
      • webpack 4中被废弃,改用mini-css-extract-plugin
    • friendly-errors-webpack-plugin^1.6.1 ==> ^1.7.0
    • html-webpack-plugin^2.30.1 ==> ^4.5.2
    • mini-css-extract-plugin:新增^1.6.2
      • 用于替代extract-text-webpack-plugin
    • optimize-css-assets-webpack-plugin:移除。
      • 改用esbuild压缩 css,以获取更快的速度。
    • speed-measure-webpack-plugin:新增^1.5.0
      • 用于检视各构建流程用时,在迁移完成后可移除
    • uglifyjs-webpack-plugin:移除。
      • webpack 4中被废弃,webpack 4默认使用terser压缩 JS,但这里改用更快的esbuild压缩。
  • Deps:

    • @babel/core:新增^7.20.5

      • babel-loader要求。
    • @babel/preset-env:新增^7.20.2

      • 生产模式下,对jsx模块进行兼容性处理。
    • @vue/babel-helper-vue-jsx-merge-props:新增^1.4.0

    • @vue/babel-preset-jsx:新增^1.4.0

      • 以上两个依赖,用于配合babelelement-ui(以及修改后的el-组件)提供jsx转译。
    • autoprefixer^7.1.2 ==> ^9.4.7

    • babel-core:移除

      • babel 7.x后替换为@babel/core
    • babel-eslint:移除

      • 以上两个依赖不再需要。
    • babel-helper-vue-jsx-merge-props:移除

    • babel-plugin-syntax-jsx:移除

    • babel-plugin-transform-runtime:移除

    • babel-plugin-transform-vue-jsx:移除

      • 以上与jsx相关依赖包含于@vue/babel-preset-jsx中。
    • babel-preset-env:移除

    • babel-preset-stage-2:移除

      • 较新版本的babel改用@babel/preset-env进行兼容。
    • node-notifier^5.1.2 ==> ^8.0.2

    • eslint-plugin-html:移除。

    • eslint-plugin-import:移除。

    • eslint-plugin-node:移除。

    • eslint-plugin-promise:移除。

    • eslint-plugin-standard:移除。

      • 以上 ESLint Plugins 均未在项目中使用到。
    • eslint-plugin-vue^5.2.2 ==> ^9.7.0

    • postcss:新增^8.4.5

      • postcss-loader要求。
    • postcss-import^11.0.0 ==> ^14.0.2

    • postcss-url^7.2.1 ==> ^10.1.3

    • portfinder^1.0.13 ==> ^1.0.28

    • vue-template-compiler^2.5.2 ==> ^2.7.10

配置文件重构

原本的webpack配置文件(/build/)通过webpack-merge实现一种inheritance(继承)式的配置复用策略,直观呈现为通用(common)、开发(dev)和生产(prod)三个庞大的对象字面量(plain object)。由于WebpackConfig的配置项繁杂,这种结构的可读性较差。

新的架构在inheritance的基础上,借鉴 SurviveJS 所提倡的 Config 组织方案,将配置分解为更小的单元,其粒度为配置中启用一个特定功能所对应的部分,如loaderdevServer等。

并且,Node.js 逐渐支持 ES Module 模块系统,而 ESM 作为一种静态模块系统能提供比 CJS 更好的类型推断能力。虽然 v2 项目本身不使用 TypeScript,但若使用的第三方库编写了类型声明,可以配合 IDE 提供类型提示,故改用 ESM 编写了配置文件。

v2 (before)
├─ build
│ ├─ ...
│ ├─ webpack.base.conf.js // 通用
│ ├─ webpack.dev.conf.js // 开发
│ └─ webpack.prod.conf.js // 生产

v2 (after)
├─ build
│ ├─ ...
│ ├─ webpack.common.mjs // 通用
│ ├─ webpack.dev.mjs // 开发
│ └─ webpack.prod.mjs // 生产

构建效率优化

使用esbuild代替babel

esbuild是基于 Go 的新一代 JavaScript 转译器,相比用 JS 编写的babel,具有性能上的天然优势。

esbuild为什么比其他 bundler 快?
  • 用 Go 编写,并编译成了原生代码
    • 常见的 bundler 用 JS 编写,而对于即时编译(JIT-compiled)语言来说,命令行应用(command-line application)是其性能最差的场景。每次运行 bundler 时,JavaScript VM(V8)需要在没有任何优化提示的情况下先编译 bundler 的 JS 源码。而 esbuild 的原生代码产物可以直接运行(即开始打包)。
  • 大量使用并发
    • 另外,Go 是以并发为核心设计的,它具有共享内存的 IPC 方式。Go 和 JavaScript 都有并行的垃圾回收器,但是 Go 的是线程间共享的,而每个 JavaScript 线程有一个单独的堆,这使得 JavaScript 的可行并行量减少了一半,因为一半 的 CPU 核在为另一半收集垃圾。
  • esbuild 是从头开始编写的(from scratch)
    • esbuild 是以性能为第一需求而从头编写的,相比于使用第三方库,从头编写可以保证所有地方使用的数据结构一致,避免了昂贵的数据结构转换。
    • 以 TypeScript 为例,许多 bundler 会使用官方的 tsc 进行转译,但官方的编译器不是以性能为第一追求的,其内部大量使用了 megamorphic object shapes 和不必要的动态属性访问(dynamic property access),大大拖慢 JS 执行速度。
  • 对内存的高效使用
    • esbuild 只操作 JavaScript AST 三次
      • 词法分析(lexing)、解析、建立作用域和声明标识符;
      • 绑定标识符、JSX/TS => JS、ESNEXT => ES2015;
      • 最小化(minification)、代码生成、source map 生成。
    • 这样的设计可以最大程度地重复利用 AST 数据,因为数据依然活跃在 CPU 缓存(cache)中。
    • 而其他 bundler 会串行、而不是交织执行以上任务,且过程中 AST 可能会多次被转换成其他形式:string -> TS -> JS -> stringstring -> JS -> older JS -> stringstring -> JS -> minified JS -> string。这会占用更多内存并降低处理速度。

考虑到 v2 项目本身的适配对象是基于 Chromium 的 Chrome、Edge 等现代浏览器,契合esbuild本身支持的最低编译目标es2015,因此改用esbuild转译 JS 可行

在代替babel时,将原来的babel-loader及其相关依赖(presetsplugins)全部移除,并配置esbuild-loader如下:

// build\webpack.common.js
{
	loader: 'esbuild-loader',
	// ...
	options: {
		target: 'es2015', // esbuild 的最低编译目标为 es2015
	}
}

在转译之外,使用esbuild对 js、css 进行压缩,可以获得比tersercssnano等传统压缩库更快的速度。

// build\webpack.prod.js
{
  // ...
  optimization: {
    // ...
    minimizer: [
      new ESBuildMinifyPlugin({
        target: 'es2015',
        css: true, // 压缩 css
      }),
    ]
  }
}

注意

由于element-uibabel硬性依赖esbuild无法正确处理其jsx语法的转译,所以项目中涉及到element-ui的模块(包括components/modify-component/等目录下,修改过的el-xxx组件),仍需使用babel进行转译和兼容。

寻址优化

寻址优化是webpack的传统优化手段之一,即通过合理设置 loader 的excludeinclude属性,只对必要的模块执行解析,减少构建的任务量。

开发模式下,由于开发者自己使用较新的浏览器版本,不需要对es6+的语法进行兼容,仅转译浏览器原生不支持的jsx语法即可。配置babel-loader,只处理包含jsx语法的模块,其他原生 JS 文件和.vue文件生成的 JS 模块则不经过 loader。

// build\webpack.dev.js
{
	test: /.m?jsx?$/,
	loader: 'babel-loader',
	// 开发模式下, 只需转译使用了 JSX 的模块
	include: [
		// 所有使用了 JSX 语法的模块
		// module1
		// module2
		// ...
	],
	options: {
		presets: [
			'@vue/babel-preset-jsx'
		]
	}
}

生产模式下,为了兼容性,应针对所有使用了浏览器原生不支持的语法(如jsx)和高于es2015版本语法的 JS 模块进行转译。

// build\webpack.prod.js
const configureBabelLoader = () => ({
  test: /.m?jsx?$/,
  loader: 'babel-loader',
  // babel 用于处理 element-ui 相关组件
  include: [
    // 所有使用了 JSX 语法的模块
    // module1
    // module2
    // ...
  ],
  options: {
    presets: [
      '@vue/babel-preset-jsx',
      [
        '@babel/preset-env',
        {
          targets: {
            // Chorme 58 和 Edge 14 分别是其完全支持 es2015 的最早版本
            chrome: '58',
            edge: '14',
          },
        },
      ],
    ],
  },
})

const configureESBuildLoader = () => ({
  test: /\.m?js$/,
  loader: 'esbuild-loader',
  // esbuild 用于处理 element-ui 以外的所有 JS 模块
  include: [
    // 所有未使用 JSX 语法的 JS 模块
    // module1
    // module2
    // ...
  ],
  exclude: [
    // 排除使用了 JSX 语法的模块
  ],
  options: {
    // 生产模式下, 需要兼容到 es2015 语法
    target: 'es2015',
  },
})

生产构建关闭 ESLint

在构建时,eslint提供的代码静态检查会占据不少时间,而这些提示信息在生产构建阶段价值不大。因此在生产构建时,移除eslint

生产模式不生成 SourceMap

SourceMap 是结合浏览器 DevTool 将打包产物映射回源码的工具文件,在构建时生成和输出 SourceMap 会耗费大量时间,且产物占据大量内存,考虑到项目没有远程调试的需求,应当可以移除 SourceMap。

\build\webpack.prod.js

{
	devtool: false // 关闭 SourceMap
	// ...
}

另外,vite在生产构建默认不生成 SourceMap,也可作为本项改动的理论支撑之一。

webpack 4版本特定的一些优化

Reference

垃圾回收 (GC)

Reference

webpack 4使用全局string.replace(/.../g, '...')来生成控制台信息,这会导致大量的垃圾回收。

虽然 V8 的垃圾回收进程是并行的,但其采用管线化的 IPC 方式,所以 GC 仍会带来较大的进程间通信开销

开发模式下,关闭控制台输出模块路径信息的功能:

// build\webpack.dev.js
{
  // ...
  output: {
    pathinfo: false // Tell webpack not to include comments in bundles with information about the contained modules.
    // ...
  }
}

webpack在生产模式下默认不展示pathinfo,不需要另外配置。

关闭开发模式下的一些默认优化

optimization及其splitChunksremoveEmptyChunks设置为false可提升webpack 4开发环境下的性能。

// build\webpack.dev.js
{
	optimization: {
		splitChunks: {
			cacheGroups: {
				default: false
			}
		},
		removeEmptyChunks: false
	}
}

文件级缓存

使用hard-source-webpack-plugin,为模块提供中间缓存(于磁盘中),应用后会使第一次构建速度变慢(因为要生成磁盘文件和hash),而后续构建速度会大大提升

对于迁移后的 v2,首次构建会生成大约 250 MB 的缓存文件,但后续构建速度提升非常夸张

注意hard-source-webpack-pluginspeed-measure-webpack-plugin不兼容,故目前为了测量构建速度,未开启hard-source-webpack-plugin

迁移到 Vue 2.7

更新依赖:

  • vue^2.5.3 ==> ^2.7.14
  • vue-template-compiler:移除。
    • vue 2.7已不需要该依赖。

迁移后,经测试可使用 Vue 2.x 版本的 Composition API 和<script setup>语法。

由于项目对element-ui的一些组件进行了重写,而element-ui的源码后续更新时变动较大,所以不能直接将element-ui也升级到最新版本。

注意

因与迁移webpack的工程化意义不同,迁移vue属于更偏应用层的更新,可在团队对业务代码层有额外的明确需求时再进行,如:

  • 为了更好的代码组织、逻辑复用或向 Vue 3 过渡,从 Options API 迁移到 Composition API;
  • 为提高项目的可维护性,加入 TypeScript;

但如果实际迁移到 Vue 2.7,应该也能稍微提升构建效率,因为不再需要配置webpack为 Node.js 注入 polyfill。

Bug Fixing

js-base64幽灵依赖问题

幽灵依赖(Phantom Dependencies)是指由于node_modules扁平的文件结构设计,在package.json未声明的依赖却可以被单独显式引入的依赖。

在 v2 中,js-base64在一些组件中被显式引入,但package.json中未对其声明,这是postcss依赖于js-base64所导致。本次迁移中将js-base64^2.6.4加入依赖。

Benchmarking

Methodology

Reference:

开发模式,冷启动:

  • 运行npm run dev,记录webpack-cli在控制台打印的信息即可。

HMR:

  • 服务端(DevServer)- 记录控制台webpack-cli打印的构建时间即可。
    • Root - 在App.vue添加HMR-n(n from 1 to 5),记录每次 recompile 用时;
    • Leaf - 在xxx.vue修改姓名姓名n(n from 1 to 5),记录每次 recompile 用时。
  • 客户端(Browser)- 另开一个 node 进程,通过fs.watch()监听文件修改并打印时间戳,求与客户端渲染时间戳的差值
    • Root - 在App.vue添加n-{{Date.now()}}(n from 1 to 5),记录控制台与客户端显示时间戳;
    • Leaf - 在xxx.vue添加n-{{Date.now()}}(n from 1 to 5),记录控制台与客户端显示时间戳。

监听文件修改的脚本:

// watch.js
// `node watch.js`运行即可
const fs = require('fs')

fs.watch('./src/App.vue', (e, filename) => {
  console.log(Date.now(), filename)
})

fs.watch('./src/views/taskmanager/task/task.vue', (e, filename) => {
  console.log(Date.now(), filename)
})

生产模式,打包:

  • 运行npm run build,记录webpack-cli在控制台打印的信息即可。

Comparison

Dev

  • 迁移前(main分支,#38fc8版本):
Times 1 2 3 4 5 Average
Cold Start (ms) 98272 93993 89986 94089 84342 92136
HMR Server Root (ms) 2714 2729 2536 2567 2324 2574
HMR Server Leaf (ms) 2961 2836 2928 2628 2602 2791
HMR Client Root (ms) 3927 3657 3691 3609 3512 3679
HMR Client Leaf (ms) 9688 4015 3962 3480 4067 3881
  • 迁移后(migrate2webpack@4分支,#26b08版本):
Times 1 2 3 4 5 Average
Cold Start (ms) 44887 45776 42537 42784 47785 44754 (51% faster)
HMR Server Root (ms) 840 759 616 646 611 694 (73% faster)
HMR Server Leaf (ms) 1019 766 788 745 685 800 (71% faster)
HMR Client Root (ms) 1382 1156 1188 1096 1070 1178 (68% faster)
HMR Client Leaf (ms) 1588 1747 1293 1236 1276 1428 (65% faster)

Prod

  • 迁移前(main分支,#e1b47c6版本):
Times 1 2 3 4 5 6 Average
Build Time (s) 125.45 173.37 171.40 181.32 140.26 153.77 157.60
  • 迁移后(migrate2webpack@4分支,#8333ce4版本):
Times 1 2 3 4 5 6 Average
Build Time (s) 77.99 71.34 98.76 64.17 73.88 88.27 79.07 (49.83% faster)

Miscellaneous

构建产物优化

分离webpack运行时

Reference:

webpack运行时(runtime)包含了项目初始需要加载哪些文件的清单(manifest),当需要加载的文件名(hash)改变时,运行时会使旧版的文件失效

结合为产物命名添加的hash,将运行时单独分离出来可促使对客户端缓存的有效利用。

// build\webpack.prod.js
{
  // ...
  optimization: {
    // ...
    runtimeChunk: {
      name: 'runtime'
    }
  }
}

这个策略与 v2 原本分离的manifest块作用基本一致。

Bundle Splitting

Vendor separating

在原本的 v2 项目中,将所有来自node_modules的依赖都打包到一个vendor块中,这会生成一个体积庞大的 JS 文件,且这个 JS 文件将在项目启动时就会被加载,导致首屏性能下降,但可提升后续访问性能。

由于该配置有利有弊,故迁移后依然保留了这个配置:

// build\webpack.prod.js
{
	// ...
	optimization: {
		splitChunks: {
			cacheGroups: {
				commons: {
					name: 'vendor',
					test: /[\\/]node_modules[\\/]/,
					chunks: 'initial',
					priority: 0
				},
				// ... other chunks
			}
		}
	}
}

实际上,webpack 4在生产模式下已经默认启用了一种分块策略,这种策略已经可以满足大部分情况下的构建需要。

webpack 4生产模式下的默认分块策略
  • 可被共享的代码块来自node_modules的模块;
  • 压缩前,体积超过 20kb 的代码块;
  • 按需的最大并行请求数低于等于 30;
  • 首屏加载的最大并行请求数低于等于 30。

可由团队协商后,决定是继续沿用该策略还是使用webpack 4自带的分块策略。

CSS extraction

v2 原本通过extract-text-webpack-plugin将项目的所有 CSS 代码提取到单独一个文件中,与上面的vendor一样,其导出的 CSS 文件体积庞大,将阻塞应用的首屏渲染。

迁移后,由于extract-text-webpack-pluginwebpack 4中被废弃,改用mini-css-extract-plugin在生产环境下进行 CSS 处理,而mini-css-extract-plugin设计理念就在于为每个包含 CSS 代码的 JS 文件生成单独的 CSS 文件,以提供异步加载、提高客户端缓存利用等特性。

vite也针对 CSS 默认开启了分块策略,而不是将整个项目的 CSS 提取到单个文件中。

与上面的 Vendor Separating 相同,目前仍保留了这个配置,迁移后通过配置webpack 4SplitChunksPlugin实现:

// build\webpack.prod.js
{
	// ...
	optimization: {
		splitChunks: {
			cacheGroups: {
				styles: {
					name: 'styles',
					test: /\.(css)|(styl(us)?)|(vue)$/,
					chunks: 'all',
					priority: 5
				},
				// ... other chunks
			}
		}
	}
}

注意

可能是由于webpack 4SplitChunksPlugin的固有特性,该配置在提取 CSS 之外,引入了将相关 JS 模块也合并在一起的副作用。在webpack 5中,该副作用貌似可以通过用types: 'css/mini-extract'代替test: /\.(css)|(styl(us)?)|(vue)$/避免。

关于是继续沿用提取唯一一个 CSS 文件还是为每个模块生成单独的 CSS 文件,可由团队在商议并进行测试后决议。

关于 ESLint 的配置

在 v1、v2 与 h5 里,只有 v2 是在webpack构建时启用了 eslint的。

h5 的eslint提示是 IDE 读取.eslintrc.js并配合 ESLint 扩展所提供的。

// v1 本身就没有安装 eslint 依赖

// h5, build\webpack.base.conf.js
module: {
  rules: [
    ...(config.dev.useEslint ? [] : []), // h5 中,关于是否应用eslint的配置处始终返回空数组
  ]
}

而 v2 中,原有的eslint配置如下:

// .eslintrc.js
module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module',
    allowImportExportEverywhere: true,
  },
  env: {
    browser: true,
    node: true,
    es6: true,
  },
  globals: {
    // ...
  },
  extends: 'eslint:recommended', // 注意此处 extends 配置
  plugins: [
    // 注意此处 plugins 配置
    'html',
    'vue',
  ],
  rules: {
    // custom rules ...
  },
}

其中有两个值得注意的点:

  • plugins: ['html', 'vue'] - 将插件eslint-plugin-htmleslint-plugin-vue接入eslint,但这些插件只是为eslint提供了静态分析.html.vue文件的能力,并不会自动引入任何规则集
  • extends: 'eslint:recommended' - extends选项eslint引入了eslint:recommended这一规则集,并没有引入package.json所声明的包括eslint-plugin-vue在内的各插件内置的规则。

且在更新eslint相关依赖后,新版的eslint:recommended增加了许多error等级的规则,导致迁移后构建时新增许多报错。因代码规范需要整个团队协商决定,故目前在migrate2webpack@4分支上未开启eslint-webpack-plugin,可由团队确定具体措施后,予以开启。

或者也可像 h5 一样,关闭构建时的eslint,通过 IDE 扩展实时地对代码进行静态分析。

注意

  • 在迁移后,考虑到项目需要,eslint相关依赖只保留了eslint-plugin-vue,为vue组件提供的额外规则集,可由团队商议后决定是否应用。
  • 另外,在开发环境下开启eslint必然会增加构建工作量、降低构建速度,所以建议开启eslint-webpack-plugin缓存配置,这可加快后续的构建效率。
    • eslint-webpack-plugin配置cache: true,会在 linting 后生成一个.eslintcache文件,已在.gitignore中声明忽略该文件。

Alternatives

在优化构建效率的过程中,尝试了其他一些来自社区的方法,但由于效果不佳,没有应用到最终版本中,现将其列举如下,可后续再考虑是否应用。

缓存

使用cache-loader对大开销 loader 处理结果进行缓存,构建时对比模块的内容hash决定是重新构建还是复用过去结果。

在自己的机器上对vue-loadercss-loaderstylus-loader等几个大开销 loader 应用后效果不佳,故未采用。

猜想可能是引入的cache-loader增加了读取和写入文件的开销,使得效率提升不高,可能因机器而异。

多线程

使用happypackthread-loader为大开销 loader 开启多线程,通过并发提升打包效率。

同样,在自己的机器上对不同的 loader 应用thread-loader并应用不同的配置(线程数、线程最大休眠时间等)后,效果并不佳,故未采用。

thread-loader开启后,线程和线程之间通信会带来额外的开销,猜想可能是处理.vue模块时对<template><script><style>块的处理需要各线程频繁通信,导致拖慢构建效率。

output.futureEmitAssets

自从 webpack^4.29,配置output.futureEmitAssets = true可使webpack 4应用webpack 5的构建逻辑,根据官方文档,这会在输出资源后释放内存,从而提升构建效率。

output.futureEmitAssets 的作用
  • Compilation.assets中的资源被替换为SizeOnlySource
  • 不允许生成资源后再读取资源;
  • 这使得内存可以被垃圾回收

在自己引入后,发现开发环境下的构建效率并未提高;而生产环境若使用 CI/CD 的模式进行发包(即每次都是全新的环境),则对内存的清理并不重要,故未采用。

注意

官方文档提到,开启这个配置可能导致那些会假定之前输出的资源仍在内存中的插件出错,但这类插件在整个生态中的占比非常小,且该逻辑在webpack 5中已是默认开启的,所以如果决定启用该配置,并不需要在插件适配上有太多顾虑。

Node polyfills

原来配置的 boilerplate 中为vue配置了一些 Node.js 模块的 polyfill,注入 polyfill 会导致降低构建速度且使构建产物体积增大,在将vue升级到vue^2.7后,便可以将其完全移除。

Drawbacks

  • 本次优化是针对项目宏观层面的,虽然构建成功,但不能保障每个运行时不出错,而这可能需要对 v2 相关业务做全面的测试才能得知;
  • 本次优化是仅针对 v2 的,并将node.js的版本升至^14.20.0,与 v1 差距较大,若发布生产时 v1、v2 使用统一的node环境,则 v1 的构建可能出错。

Adoption strategy

本次迁移在migrate2webpack@4分支上开发,在团队进行评估,并通过开发、生产环境的测试后,可选取一个版本合并该分支。

关于测试

  • 由于该迁移是对整个 v2 前端基础设施的升级,与具体业务无关,所以可能需要特殊的测试策略。

合并流程

以下提供一种合并设想,假设目标版本分支为target-branch:

  1. git checkout migrate2webpack@4,切换到migrate2webpack@4分支;
  2. git rebase target-branch,将迁移相关提交提升至历史栈顶;
  3. git reset --soft <hash-of-the-last-commit-on-target-branch>,将迁移相关提交还原至暂存区;
  4. git commit -m 'migration: migrate webpack 3 to webpack 4',合并所有迁移相关修改到一次提交中,避免污染提交历史;
    • 原本的提交中一些地方包含了修改相关的注释,若有必要,可以migrate2webpack@4为原型新建一个分支,备份原来的提交信息;
  5. git checkout target-branch,切换到target-branch
  6. git merge migrate2webpack@4,将迁移代码合并到该版本,后续合并到开发分支上。

注意

  • migrate2webpack@4分支最后一次rebase的目标是main #5238cfb1