凡科建站登陆,网件官网,号卡分销系统搭建,关键词营销seo项目背景
我们的系统#xff08;一个 ToB 的 Web 单页应用#xff09;前端单页应用经过多年的迭代#xff0c;目前已经累积有大几十万行的业务代码#xff0c;30 路由模块#xff0c;整体的代码量和复杂度还是比较高的。
项目整体是基于 Vue TypeScirpt#xff0c;而构…项目背景
我们的系统一个 ToB 的 Web 单页应用前端单页应用经过多年的迭代目前已经累积有大几十万行的业务代码30 路由模块整体的代码量和复杂度还是比较高的。
项目整体是基于 Vue TypeScirpt而构建工具由于最早项目是经由 vue-cli 初始化而来所以自然而然使用的是 Webpack。
我们知道随着项目体量越来越大我们在开发阶段将项目跑起来也就是通过 npm run serve 的单次冷启动时间以及在项目发布时候的 npm run build 的耗时都会越来越久。
因此打包构建优化也是伴随项目的成长需要持续不断去做的事情。在早期项目体量比较小的时构建优化的效果可能还不太明显而随着项目体量的增大构建耗时逐渐增加如何尽可能的降低构建时间则显得越来越重要 大项目通常是团队内多人协同开发单次开发时的冷启动时间的降低乘上人数及天数经年累月节省下来的时间非常可观能较大程度的提升开发效率、提升开发体验 大项目的发布构建的效率提升能更好的保证项目发布、回滚等一系列操作的准确性、及时性 本文就将详细介绍整个 WMS FE 项目在随着项目体量不断增大的过程中对整体的打包构建效率的优化之路。
瓶颈分析
再更具体一点我们的项目最初是基于 vue-cli 4当时其基于的是 webpack4 版本。如无特殊说明下文的一些配置会基于 webpack4 展开。
工欲善其事必先利其器解决问题前需要分析问题要优化构建速度首先得分析出 Webpack 构建编译我们的项目过程中耗时所在侧重点分布。
这里我们使用的是 SMP 插件统计各模块耗时数据。 speed-measure-webpack-plugin 是一款统计 webpack 打包时间的插件不仅可以分析总的打包时间还能分析各阶段loader 的耗时并且可以输出一个文件用于永久化存储数据。 // 安装npm install --save-dev speed-measure-webpack-plugin
// 使用方式
const SpeedMeasurePlugin require(speed-measure-webpack-plugin);const smp new SpeedMeasurePlugin();config.plugins.push(smp());
开发阶段构建耗时
对于 npm run serve也就是开发阶段而言在没有任何缓存的前提下单次冷启动整个项目的时间达到了惊人的 4 min。 生产阶段构建耗时
而对于 npm run build也就是实际线上生产环境的构建看看总体的耗时 因此对于构建效率的优化可谓是势在必行。首先我们需要明确优化分为两个方向 基于开发阶段 npm run serve 的优化
在开发阶段我们的核心目标是在保有项目所有功能的前提下尽可能提高构建速度保证开发时的效率所以对于 Live 才需要的一些功能譬如代码混淆压缩、图片压缩等功能是可以不开启的并且在开发阶段我们需要热更新。 基于生产阶段 npm run build 的优化
而在生产打包阶段尽管构建速度也非常重要但是一些在开发时可有可无的功能必须加上譬如代码压缩、图片压缩。因此生产构建的目标是在于保证最终项目打包体积尽可能小所需要的相关功能尽可能完善的前提下同时保有较快的构建速度。
两者的目的不尽相同因此一些构建优化手段可能仅在其中一个环节有效。
基于上述的一些分析本文将从如下几个方面探讨对构建效率优化的探索 基于 Webpack 的一些常见传统优化方式 分模块构建 基于 Vite 的构建工具切换 基于 Es-build 插件的构建效率优化 为什么这么慢
那么为什么随着项目的增大构建的效率变得越来越慢了呢
从上面两张截图不难看出对于我们这样一个单页应用构建过程中的大部分时间都消耗在编译 JavaScript 文件及 CSS 文件的各类 Loader 上。
本文不会详细描述 Webpack 的构建原理我们只需要大致知道Webpack 的构建流程主要时间花费在递归遍历各个入口文件并基于入口文件不断寻找依赖逐个编译再递归处理的过程每次递归都需要经历 String-AST-String 的流程然后通过不同的 loader 处理一些字符串或者执行一些 JavaScript 脚本由于 NodeJS 单线程的特性以及语言本身的效率限制Webpack 构建慢一直成为它饱受诟病的原因。
因此基于上述 Webpack 构建的流程及提到的一些问题整体的优化方向就变成了 缓存 多进程 寻路优化 抽离拆分 构建工具替换 基于 Webpack 的传统优化方式
上面也说了构建过程中的大部分时间都消耗在递归地去编译 JavaScript 及 CSS 的各类 Loader 上并且会受限于 NodeJS 单线程的特性以及语言本身的效率限制。
如果不替换掉 Webpack 本身语言本身NodeJS的执行效率是没法优化的只能在其他几个点做文章。
因此在最早期我们所做的都是一些比较常规的优化手段这里简单介绍最为核心的几个 缓存 多进程 寻址优化 缓存优化
其实对于 vue-cli 4 而言已经内置了一些缓存操作譬如上图可见到 loader 的过程中有使用 cache-loader所以我们并不需要再次添加到项目之中。 cache-loader: 在一些性能开销较大的 loader 之前添加 cache-loader以便将结果缓存到磁盘里
那还有没有一些其他的缓存操作呢用上的呢我们使用了一个 HardSourceWebpackPlugin。
HardSourceWebpackPlugin HardSourceWebpackPlugin: HardSourceWebpackPlugin 为模块提供中间缓存缓存默认存放的路径是 node_modules/.cache/hard-source配置了 HardSourceWebpackPlugin 之后首次构建时间并没有太大的变化但是第二次开始构建时间将会大大的加快。
首先安装依赖
npm install hard-source-webpack-plugin -D
修改 vue.config.js 配置文件
const HardSourceWebpackPlugin require(hard-source-webpack-plugin);
module.exports {...configureWebpack: (config) {// ...config.plugins.push(new HardSourceWebpackPlugin());},...
}配置了 HardSourceWebpackPlugin 的首次构建时间和预期的一样并没有太大的变化但是第二次构建从平均 4min 左右降到了平均 20s 左右提升的幅度非常的夸张当然这个也因项目而异但是整体而言在不同项目中实测发现它都能比较大的提升开发时二次编译的效率。
设置 babel-loader 的 cacheDirectory 以及 DLL
另外在缓存方面我们的尝试有 设置 babel-loader 的 cacheDirectory DLL 但是整体收效都不太大可以简单讲讲。
打开 babel-loader 的 cacheDirectory 的配置当有设置时指定的目录将用来缓存 loader 的执行结果。之后的 webpack 构建将会尝试读取缓存来避免在每次执行时可能产生的、高性能消耗的 Babel 重新编译过程。实际的操作步骤你可以看看 Webpack - babel-loader。
那么 DLL 又是什么呢
DLL 文件为动态链接库在一个动态链接库中可以包含给其他模块调用的函数和数据。
为什么要用 DLL
原因在于包含大量复用模块的动态链接库只需要编译一次在之后的构建过程中被动态链接库包含的模块将不会在重新编译而是直接使用动态链接库中的代码。
由于动态链接库中大多数包含的是常用的第三方模块例如 Vue、React、React-dom只要不升级这些模块的版本动态链接库就不用重新编译。
DLL 的配置非常繁琐并且最终收效甚微我们在过程中借助了 autodll-webpack-plugin感兴趣的可以自行尝试。值得一提的是Vue-cli 已经剔除了这个功能。
多进程
基于 NodeJS 单线程的特性当有多个任务同时存在它们也只能排队串行执行。
而如今大多数 CPU 都是多核的因此我们可以借助一些工具充分释放 CPU 在多核并发方面的优势利用多核优势多进程同时处理任务。
从上图中可以看到Vue CLi4 中其实已经内置了 thread-loader。 thread-loader: 把 thread-loader 放置在其它 loader 之前那么放置在这个 loader 之后的 loader 就会在一个单独的 worker 池中运行。这样做的好处是把原本需要串行执行的任务并行执行。
那么除了 thread-loader还有哪些可以考虑的方案呢
HappyPack
HappyPack 与 thread-loader 类似。
HappyPack 可利用多进程对文件进行打包, 将任务分解给多个子进程去并行执行子进程处理完后再把结果发送给主进程达到并行打包的效果、HappyPack 并不是所有的 loader 都支持, 比如 vue-loader 就不支持。
可以通过 Loader Compatibility List 来查看支持的 loaders。需要注意的是创建子进程和主进程之间的通信是有开销的当你的 loader 很慢的时候可以加上 happypack。否则可能会编译的更慢。
当然由于 HappyPack 作者对 JavaScript 的兴趣逐步丢失维护变少webpack4 及之后都更推荐使用 thread-loader。因此这里没有实际结论给出。 上一次 HappyPack 更新已经是 3 年前 寻址优化
对于寻址优化总体而言提升并不是很大。
它的核心即在于合理设置 loader 的 exclude 和 include 属性。 通过配置 loader 的 exclude 选项告诉对应的 loader 可以忽略某个目录 通过配置 loader 的 include 选项告诉 loader 只需要处理指定的目录loader 处理的文件越少执行速度就会更快 这肯定是有用的优化手段只是对于一些大型项目而言这类优化对整体构建时间的优化不会特别明显。
分模块构建
在上述的一些常规优化完成后。整体效果仍旧不是特别明显因此我们开始思考一些其它方向。
我们再来看看 Webpack 构建的整体流程 上图是大致的 webpack 构建流程简单介绍一下 entry-option读取 webpack 配置调用 new Compile(config) 函数准备编译 run开始编译 make从入口开始分析依赖对依赖模块进行 build before-resolve对位置模块进行解析 build-module开始构建模块 normal-module-loader生成 AST 树 program遍历 AST 树遇到 require 语句收集依赖 sealbuild 完成开始优化 emit输出 dist 目录 随着项目体量地不断增大耗时大头消耗在第 7 步递归遍历 AST解析 require如此反复直到遍历完整个项目。
而有意思的是对于单次单个开发而言极大概率只是基于这整个大项目的某一小个模块进行开发即可。
所以如果我们可以在收集依赖的时候跳过我们本次不需要的模块或者可以自行选择只构建必要的模块那么整体的构建时间就可以大大减少。
这也就是我们要做的 -- 分模块构建。
什么意思呢举个栗子假设我们的项目一共有 6 个大的路由模块 A、B、C、D、E、F当新需求只需要在 A 模块范围内进行优化新增那么我们在开发阶段启动整个项目的时候可以跳过 B、C、D、E、F 这 5 个模块只构建 A 模块即可 假设原本每个模块的构建平均耗时 3s原本 18s 的整体冷启动构建耗时就能下降到 3s。
分模块构建打包的原理
Webpack 是静态编译打包的Webpack 在收集依赖时会去分析代码中的 requireimport 会被 bebel 编译成 require 语句然后递归的去收集依赖进行打包构建。
我们要做的就是通过增加一些配置简单改造下我们的现有代码使得 Webpack 在初始化遍历整个路由模块收集依赖的时候可以跳过我们不需要的模块。
再说得详细点假设我们的路由大致代码如下
import Vue from vue;
import VueRouter, { Route } from vue-router;// 1. 定义路由组件.
// 这里简化下模型实际项目中肯定是一个一个的大路由模块从其他文件导入
const moduleA { template: divAAAA/div }
const moduleB { template: divBBBB/div }
const moduleC { template: divCCCC/div }
const moduleD { template: divDDDD/div }
const moduleE { template: divEEEE/div }
const moduleF { template: divFFFF/div }// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routesConfig [{ path: /A, component: moduleA },{ path: /B, component: moduleB },{ path: /C, component: moduleC },{ path: /D, component: moduleD },{ path: /E, component: moduleE },{ path: /F, component: moduleF }
]const router new VueRouter({mode: history,routes: routesConfig,
});// 让路由生效 ...
const app Vue.createApp({})
app.use(router)我们要做的就是每次启动项目时可以通过一个前置命令行脚本收集本次需要启动的模块按需生成需要的 routesConfig 即可。
我们尝试了 IgnorePlugin 插件 webpack-virtual-modules 配合 require.context NormalModuleReplacementPlugin 插件进行文件替换 最终选择了使用 NormalModuleReplacementPlugin 插件进行文件替换的方式原因在于它对整个项目的侵入性非常小只需要添加前置脚本及修改 Webpack 配置无需改变任何路由文件代码。总结而言该方案的两点优势在于 无需改动上层代码 通过生成临时路由文件的方式替换原路由文件对项目无任何影响 使用 NormalModuleReplacementPlugin 生成新的路由配置文件
利用 NormalModuleReplacementPlugin 插件可以不修改原来的路由配置文件在编译阶段根据配置生成一个新的路由配置文件然后去使用它这样做的好处在于对整个源码没有侵入性。
NormalModuleReplacementPlugin 插件的作用在于将目标源文件的内容替换为我们自己的内容。
我们简单修改 Webpack 配置如果当前是开发环境利用该插件将原本的 config.ts 文件替换为另外一份代码如下
// vue.config.js
if (process.env.NODE_ENV development) {config.plugins.push(new webpack.NormalModuleReplacementPlugin(/src\/router\/config.ts/,../../dev.routerConfig.ts))
}上面的代码功能是将实际使用的 config.ts 替换为自定义配置的 dev.routerConfig.ts 文件那么 dev.routerConfig.ts 文件的内容又是如何产生的呢其实就是借助了 inquirer 与 EJS 模板引擎通过一个交互式的命令行问答选取需要的模块基于选择的内容动态的生成新的 dev.routerConfig.ts 代码这里直接上代码。
改造一下我们的启动脚本在执行 vue-cli-service serve 前先跑一段我们的前置脚本
{// ...scripts: {- dev: vue-cli-service serve, dev: node ./script/dev-server.js vue-cli-service serve,},// ...
}而 dev-server.js 所需要做的事就是通过 inquirer 实现一个交互式命令用户选择本次需要启动的模块列表通过 ejs 生成一份新的 dev.routerConfig.ts 文件。
// dev-server.js
const ejs require(ejs);
const fs require(fs);
const child_process require(child_process);
const inquirer require(inquirer);
const path require(path);const moduleConfig [moduleA,moduleB,moduleC,// 实际业务中的所有模块
]//选中的模块
const chooseModules [home
]function deelRouteName(name) {const index name.search(/[A-Z]/g);const preRoute path.resolve(__dirname, ../src/router/modules/) /;if (![0, -1].includes(index)) {return preRoute (name.slice(0, index) - name.slice(index)).toLowerCase();}return preRoute name.toLowerCase();;
}function init() {let entryDir process.argv.slice(2);entryDir [...new Set(entryDir)];if (entryDir entryDir.length 0) {for(const item of entryDir){if(moduleConfig.includes(item)){chooseModules.push(item);}}console.log(output: , chooseModules);runDEV();} else {promptModule();}
}const getContenTemplate async () {const html await ejs.renderFile(path.resolve(__dirname, router.config.template.ejs), { chooseModules, deelRouteName }, {async: true});fs.writeFileSync(path.resolve(__dirname, ../dev.routerConfig.ts), html);
};function promptModule() {inquirer.prompt({type: checkbox,name: modules,message: 请选择启动的模块, 点击上下键选择, 按空格键确认(可以多选), 回车运行。注意: 直接敲击回车会全量编译, 速度较慢。,pageSize: 15,choices: moduleConfig.map((item) {return {name: item,value: item,}})}).then((answers) {if(answers.modules.length0){chooseModules.push(...moduleConfig)}else{chooseModules.push(...answers.modules)}runDEV();});
}init();模板代码的简单示意
// 模板代码示意router.config.template.ejs
import { RouteConfig } from vue-router;% chooseModules.forEach(function(item){%
import %item % from %deelRouteName(item) %;
% }) %
let routesConfig: ArrayRouteConfig [];
/* eslint-disable */routesConfig [% chooseModules.forEach(function(item){%%item %,% }) %]export default routesConfig;dev-server.js 的核心在于启动一个 inquirer 交互命令行服务让用户选择需要构建的模块类似于这样 模板代码示意 router.config.template.ejs 是 EJS 模板文件chooseModules 是我们在终端输入时获取到的用户选择的模块集合数组根据这个列表我们去生成新的 routesConfig 文件。
这样我们就实现了分模块构建按需进行依赖收集。以我们的项目为例我们的整个项目大概有 20 个不同的模块几十万行代码 构建模块数耗时冷启动全量构建 20 个模块4.5min冷启动只构建 1 个模块18s有缓存状态下二次构建 1 个模块4.5s
实际效果大致如下无需启动所有模块只启动我们选中的模块进行对应的开发即可 这样如果单次开发只涉及固定的模块单次项目冷启动的时间可以从原本的 4min 下降到 18s 左右而有缓存状态下二次构建 1 个模块仅仅需要 4.5s属于一个比较大的提升。
受限于 Webpack 所使用的语言的性能瓶颈要追求更快的构建性能我们不可避免的需要把目光放在其他构建工具上。这里我们的目光聚焦在了 Vite 与 esbuild 上。
使用 Vite 优化开发时构建
Vite一个基于浏览器原生 ES 模块的开发服务器。利用浏览器去解析 imports在服务器端按需编译返回完全跳过了打包这个概念服务器随起随用。同时不仅有 Vue 文件支持还搞定了热更新而且热更新的速度不会随着模块增多而变慢。
当然由于 Vite 本身特性的限制目前只适用于在开发阶段替代 Webpack。
我们都知道 Vite 非常快它主要快在什么地方 项目冷启动更快 热更新更快 那么是什么让它这么快
Webpack 与 Vite 冷启动的区别
我们先来看看 Webpack 与 Vite 的在构建上的区别。下图是 Webpack 的遍历递归收集依赖的过程 上文我们也讲了Webpack 启动时从入口文件出发调用所有配置的 Loader 对模块进行编译再找出该模块依赖的模块再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
这一过程是非常非常耗时的再看看 Vite Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类改进了开发服务器启动时间。它快的核心在于两点 使用 Go 语言的依赖预构建Vite 将会使用 esbuild 进行预构建依赖。esbuild 使用 Go 编写并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。依赖预构建主要做了什么呢 开发阶段中Vite 的开发服务器将所有代码视为原生 ES 模块。因此Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESMVite 将有许多内部模块的 ESM 依赖关系转换为单个模块以提高后续页面加载性能。如果不编译每个依赖包里面都可能含有多个其他的依赖每个引入的依赖都会又一个请求请求多了耗时就多 按需编译返回Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码即只在当前屏幕上实际使用时才会被处理。 Webpack 与 Vite 热更新的区别
使用 Vite 的另外一个大的好处在于它的热更新也是非常迅速的。
我们首先来看看 Webpack 的热更新机制 按需编译返回Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码即只在当前屏幕上实际使用时才会被处理。 一些名词解释 Webpack-complierWebpack 的编译器将 Javascript 编译成 bundle就是最终的输出文件HMR Server将热更新的文件输出给 HMR RuntimeBunble Server提供文件在浏览器的访问也就是我们平时能够正常通过 localhost 访问我们本地网站的原因HMR Runtime开启了热更新的话在打包阶段会被注入到浏览器中的 bundle.js这样 bundle.js 就可以跟服务器建立连接通常是使用 Websocket 当收到服务器的更新指令的时候就去更新文件的变化bundle.js构建输出的文件 Webpack 热更新的大致原理是文件经过 Webpack-complier 编译好后传输给 HMR ServerHMR Server 知道哪个资源 (模块) 发生了改变并通知 HMR Runtime 有哪些变化HMR Runtime 就会更新我们的代码这样浏览器就会更新并且不需要刷新。
而 Webpack 热更新机制主要耗时点在于Webpack 的热更新会以当前修改的文件为入口重新 build 打包所有涉及到的依赖也都会被重新加载一次。
而 Vite 号称 热更新的速度不会随着模块增多而变慢。它的主要优化点在哪呢
Vite 实现热更新的方式与 Webpack 大同小异也通过创建 WebSocket 建立浏览器与服务器建立通信通过监听文件的改变向客户端发出消息客户端对应不同的文件进行不同的操作的更新。
Vite 通过 chokidar 来监听文件系统的变更只用对发生变更的模块重新加载只需要精确的使相关模块与其临近的 HMR 边界连接失效即可这样 HMR 更新速度就不会因为应用体积的增加而变慢而 Webpack 还要经历一次打包构建。所以 HMR 场景下Vite 表现也要好于 Webpack。
通过不同的消息触发一些事件。做到浏览器端的即时热模块更换热更新。通过不同事件触发更细粒度的更新目前只有 Vue 和 JSVue 文件又包含了 template、script、style 的改动做到只更新必须的文件而不是全量进行更新。在些事件分别是 connected: WebSocket 连接成功vue-reload: Vue 组件重新加载当修改了 script 里的内容时vue-rerender: Vue 组件重新渲染当修改了 template 里的内容时style-update: 样式更新style-remove: 样式移除js-update: js 文件更新full-reload: fallback 机制网页重刷新 本文不会在 Vite 原理上做太多深入感兴趣的可以通过官方文档了解更多 -- 为什么选 Vite | Vite 官方中文文档
基于 Vite 的改造相当于在开发阶段替换掉 Webpack下文主要讲讲我们在替换过程中遇到的一些问题。
基于 Vue-cli 4 的 Vue2 项目改造大致只需要 安装 Vite 配置 index.htmlVite 解析 script typemodule src... 标签指向源码 配置 vite.config.js package.json 的 scripts 模块下增加启动命令 vite: vite 当以命令行方式运行 npm run vite时Vite 会自动解析项目根目录下名为 vite.config.js 的文件读取相应配置。而对于 vite.config.js 的配置整体而言比较简单 Vite 提供了对 .scss, .sass, .less, 和 .stylus 文件的内置支持 天然的对 TS 的支持开箱即用 基于 Vue2 的项目支持可能不同的项目会遇到不同的问题根据报错逐步调试即可譬如通过一些官方插件兼容 .tsx、.jsx 当然对于项目的源码可能需要一定的改造下面是我们遇到的一些小问题 tsx 中使用装饰器导致的编译问题我们通过魔改了 vitejs/plugin-vue-jsx使其支持 Vue2 下的 jsx 由于 Vite 仅支持 ESM 语法需要将代码中的模块引入方式由 require 改为 import Sass 预处理器无法正确解析样式中的 /deep/可使用 ::v-deep 替换 其他一些小问题譬如 Webpack 环境变量的兼容SVG iCON 的兼容 对于需要修改到源码的地方我们的做法是既保证能让 Vite 进行适配同时让该改动不会影响到原本 Webpack 的构建以便在关键时刻或者后续迭代能切回 Webpack 解决完上述的一些问题后我们成功地将开发时基于 Webpack 的构建打包迁移到了 Vite效果也非常惊人全模块构建耗时只有 2.6s 至此开发阶段的构建耗时从原本的 4.5min 优化到了 2.6s: 构建模块数耗时Webpack 冷启动全量构建 20 个模块4.54minWebpack 冷启动只构建 1 个模块18sWebpack 有缓存状态下二次构建 1 个模块4.5sVite 冷启动2.6s
优化生产构建
好上述我们基本已经完成了整个开发阶段的构建优化。下一步是优化生产构建。
我们的生产发布是基于 GitLab 及 Jenkins 的完整 CI/CD 流。
在优化之前看看我们的整个项目线上发布的耗时 可以看到生产环境构建时间较长 build 平均耗时约 9 分钟整体发布构建时长在 15 分钟左右整体构建环节耗时过长 效率低下严重影响测试以及回滚 。
好那我们看看整个构建流程都需要做什么事情 其中 Build base 和 Build Region 阶段存在较大优化空间。
Build base 阶段的优化涉及到环境准备镜像拉取依赖的安装。前端能发挥的空间不大这一块主要和 SRE 团队沟通共同进行优化可以做的有增加缓存处理、外挂文件系统、将依赖写进容器等方式。
我们的优化主要关注 Build Region 阶段也就是核心关注如何减少 npm run build 的时间。
文章开头有贴过 npm run build 的耗时分析简单再贴下 一般而言 代码编译时间和代码规模正相关。
根据以往优化经验代码静态检查可能会占据比较多时间目光锁定在 eslint-loader 上。
在生产构建阶段eslint 提示信息价值不大考虑在 build 阶段去除步骤前置。
同时我们了解到可以通过 esbuild-loader 插件去替代非常耗时的 babel-loader、ts-loader 等 loader。
因此我们的整体优化方向就是 改写打包脚本引入 esbuild 插件 优化构架逻辑减少 build 阶段不必要的检查 优化前后流程对比 优化构架逻辑减少 build 阶段不必要的检查
这个上面说了还是比较好理解的在生产构建阶段eslint 提示信息价值不大考虑在 build 阶段去除步骤前置。
比如在 git commit 的时候利用 lint-staged 及 git hook 做检查 或者利用 CI 在 git merge 的时候加一条流水线任务专门做静态检查。
我们两种方式都有做简单给出接入 Gitlab CI 的代码
// .gitlab-ci.yml
stages:- eslinteslint-job:image: node:14.13.0stage: eslintscript:- npm run lint - echo eslint successretry: 1rules:- if: $CI_PIPELINE_SOURCE merge_request_event $CI_MERGE_REQUEST_TARGET_BRANCH_NAME test通过 .gitlab-ci.yml 配置文件指定固定的时机进行 lint 指令前置步骤。
改写打包脚本引入 esbuild 插件
这里我们主要借助了 esbuild-loader。
上面其实我们也有提到 esbuildVite 使用 esbuild 进行预构建依赖。这里我们借助的是 esbuild-loader它把 esbuild 的能力包装成 Webpack 的 loader 来实现 Javascript、TypeScript、CSS 等资源的编译。以及提供更快的资源压缩方案。
接入起来也非常简单。我们的项目是基于 Vue CLi 的主要修改 vue.config.js改造如下
// vue.config.js
const { ESBuildMinifyPlugin } require(esbuild-loader);module.exports {// ...chainWebpack: (config) {// 使用 esbuild 编译 js 文件const rule config.module.rule(js);// 清理自带的 babel-loaderrule.uses.clear();// 添加 esbuild-loaderrule.use(esbuild-loader).loader(esbuild-loader).options({loader: ts, // 如果使用了 ts, 或者 vue 的 class 装饰器则需要加上这个 option 配置 否则会报错 ERROR: Unexpected target: es2015,tsconfigRaw: require(./tsconfig.json)})// 删除底层 terser, 换用 esbuild-minimize-pluginconfig.optimization.minimizers.delete(terser);// 使用 esbuild 优化 css 压缩config.optimization.minimizer(esbuild).use(ESBuildMinifyPlugin, [{ minify: true, css: true }]);}
}移除 ESLint以及接入 esbuild-loader 这一番组合拳打完本地单次构建可以优化到 90 秒。 阶段耗时优化前200S移除 ESLint、接入 esbuild-loader90S
再看看线上的 Jenkins 构建耗时也有了一个非常明显的提升 前端工程化的演进及后续规划
整体而言上述优化完成后对整个项目的打包构建效率是有着一个比较大的提升的但是这并非已经做到了最好。
看看我们旁边兄弟组的 Live 构建耗时 在项目体量差不多的情况下他们的生产构建耗时npm run build在 2 分钟出头细究其原因在于 他们的项目是 React TSX我这次优化的项目是 Vue在文件的处理上就需要多过一层 vue-loader 他们的项目采用了微前端对项目对了拆分主项目只需要加载基座相关的代码子应用各自构建。需要构建的主应用代码量大大减少这是主要原因 是的后续我们还有许多可以尝试的方向譬如我们正在做的一些尝试有 对项目进行微前端拆分将相对独立的模块拆解出来做到独立部署 基于 Jenkinks 构建时在 Build Base 阶段优化的提升譬如将构建流程前置结合 CDN 做快速回滚以及将依赖预置进 Docker 容器中减少在容器中每次 npm install 时间的消耗等 同时我们也必须看到前端技术日新月异各种构建工具目不暇给。前端从最早期的刀耕火种到逐步向工程化迈进到如今的泛前端工程化囊括的各式各样的标准、规范、各种提效的工具。构建效率优化可能会处于一种一直在路上的状态。当然这里不一定有最佳实践只有最适合我们项目的实践需要我们不断地去摸索尝试。