关于前端:前端构建效率优化之路

11次阅读

共计 15161 个字符,预计需要花费 38 分钟才能阅读完成。

我的项目背景

咱们的零碎(一个 ToB 的 Web 单页利用)前端单页利用通过多年的迭代,目前曾经累积有大几十万行的业务代码,30+ 路由模块,整体的代码量和复杂度还是比拟高的。

我的项目整体是基于 Vue + TypeScirpt,而构建工具,因为最早我的项目是经由 vue-cli 初始化而来,所以自然而然应用的是 Webpack。

咱们晓得,随着我的项目体量越来越大,咱们在开发阶段将我的项目跑起来,也就是通过 npm run serve 的单次冷启动工夫,以及在我的项目公布时候的 npm run build 的耗时都会越来越久。

因而,打包构建优化也是随同我的项目的成长须要继续一直去做的事件。在晚期,我的项目体量比拟小的时,构建优化的成果可能还不太显著,而随着我的项目体量的增大,构建耗时逐步减少,如何尽可能的升高构建工夫,则显得越来越重要:

  1. 大我的项目通常是团队内多人协同开发,单次开发时的冷启动工夫的升高,乘上人数及天数,经久不息节省下来的工夫十分可观,能较大水平的晋升开发效率、晋升开发体验
  2. 大我的项目的公布构建的效率晋升,能更好的保障我的项目公布、回滚等一系列操作的准确性、及时性

本文,就将具体介绍整个 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。

<img width=”335″ alt=”image” src=”https://user-images.githubusercontent.com/8554143/172987426-76757d80-7a9f-4a05-9aa0-d2ad0a5c3941.png”>

生产阶段构建耗时

而对于 npm run build,也就是理论线上生产环境的构建,看看总体的耗时:

<img width=”332″ alt=”image” src=”https://user-images.githubusercontent.com/8554143/172807307-d6b98ebd-c3dc-41d1-bc0f-1ef5ce2ebf68.png”>

因而,对于构建效率的优化堪称是势在必行。首先,咱们须要明确,优化分为两个方向:

  1. 基于开发阶段 npm run serve 的优化

在开发阶段,咱们的外围指标是在保有我的项目所有性能的前提下,尽可能进步构建速度,保障开发时的效率,所以对于 Live 才须要的一些性能,譬如代码混同压缩、图片压缩等性能是能够不开启的,并且在开发阶段,咱们须要热更新。

  1. 基于生产阶段 npm run build 的优化

而在生产打包阶段,只管构建速度也十分重要,然而一些在开发时可有可无的性能必须加上,譬如代码压缩、图片压缩。因而,生产构建的指标是在于保障最终我的项目打包体积尽可能小,所须要的相干性能尽可能欠缺的前提下,同时保有较快的构建速度。

两者的目标不尽相同,因而一些构建优化伎俩可能仅在其中一个环节无效。

基于上述的一些剖析,本文将从如下几个方面探讨对构建效率优化的摸索:

  1. 基于 Webpack 的一些常见传统优化形式
  2. 分模块构建
  3. 基于 Vite 的构建工具切换
  4. 基于 Es-build 插件的构建效率优化

为什么这么慢?

那么,为什么随着我的项目的增大,构建的效率变得越来越慢了呢?

从下面两张截图不难看出,对于咱们这样一个单页利用,构建过程中的大部分工夫都耗费在编译 JavaScript 文件及 CSS 文件的各类 Loader 上。

本文不会详细描述 Webpack 的构建原理,咱们只须要大抵晓得,Webpack 的构建流程,次要工夫破费在递归遍历各个入口文件,并基于入口文件一直寻找依赖一一编译再递归解决的过程,每次递归都须要经验 String->AST->String 的流程,而后通过不同的 loader 解决一些字符串或者执行一些 JavaScript 脚本,因为 NodeJS 单线程的个性以及语言自身的效率限度,Webpack 构建慢始终成为它饱受诟病的起因。

因而,基于上述 Webpack 构建的流程及提到的一些问题,整体的优化方向就变成了:

  1. 缓存
  2. 多过程
  3. 寻路优化
  4. 抽离拆分
  5. 构建工具替换

基于 Webpack 的传统优化形式

下面也说了,构建过程中的大部分工夫都耗费在递归地去编译 JavaScript 及 CSS 的各类 Loader 上,并且会受限于 NodeJS 单线程的个性以及语言自身的效率限度。

如果不替换掉 Webpack 自身,语言自身(NodeJS)的执行效率是没法优化的,只能在其余几个点做文章。

因而在最晚期,咱们所做的都是一些比拟惯例的优化伎俩,这里简略介绍最为外围的几个:

  1. 缓存
  2. 多过程
  3. 寻址优化

缓存优化

其实对于 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

另外,在缓存方面咱们的尝试有:

  1. 设置 babel-loader 的 cacheDirectory
  2. 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 的 excludeinclude 属性。

  • 通过配置 loader 的 exclude 选项,通知对应的 loader 能够疏忽某个目录
  • 通过配置 loader 的 include 选项,通知 loader 只须要解决指定的目录,loader 解决的文件越少,执行速度就会更快

这必定是有用的优化伎俩,只是对于一些大型项目而言,这类优化对整体构建工夫的优化不会特地显著。

分模块构建

在上述的一些惯例优化实现后。整体成果仍旧不是特地显著,因而,咱们开始思考一些其它方向。

咱们再来看看 Webpack 构建的整体流程:

上图是大抵的 webpack 构建流程,简略介绍一下:

  1. entry-option:读取 webpack 配置,调用 new Compile(config) 函数筹备编译
  2. run:开始编译
  3. make:从入口开始剖析依赖,对依赖模块进行 build
  4. before-resolve:对地位模块进行解析
  5. build-module:开始构建模块
  6. normal-module-loader:生成 AST 树
  7. program:遍历 AST 树,遇到 require 语句收集依赖
  8. seal:build 实现开始优化
  9. 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 在收集依赖时会去剖析代码中的 require(import 会被 bebel 编译成 require)语句,而后递归的去收集依赖进行打包构建。

咱们要做的,就是通过减少一些配置,简略革新下咱们的现有代码,使得 Webpack 在初始化遍历整个路由模块收集依赖的时候,能够跳过咱们不须要的模块。

再说得具体点,假如咱们的路由大抵代码如下:

import Vue from 'vue';
import VueRouter, {Route} from 'vue-router';

// 1. 定义路由组件.
// 这里简化下模型,理论我的项目中必定是一个一个的大路由模块,从其余文件导入
const moduleA = {template: '<div>AAAA</div>'}
const moduleB = {template: '<div>BBBB</div>'}
const moduleC = {template: '<div>CCCC</div>'}
const moduleD = {template: '<div>DDDD</div>'}
const moduleE = {template: '<div>EEEE</div>'}
const moduleF = {template: '<div>FFFF</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 即可。

咱们尝试了:

  1. IgnorePlugin 插件
  2. webpack-virtual-modules 配合 require.context
  3. NormalModuleReplacementPlugin 插件进行文件替换

最终抉择了应用 NormalModuleReplacementPlugin 插件进行文件替换的形式,起因在于 它对整个我的项目的侵入性十分小,只须要增加前置脚本及批改 Webpack 配置,无需扭转任何路由文件代码。总结而言,该计划的两点劣势在于:

  1. 无需改变下层代码
  2. 通过生成长期路由文件的形式,替换原路由文件,对我的项目无任何影响

应用 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.length===0){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: Array<RouteConfig> = [];
/* 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 十分快,它次要快在什么中央?

  1. 我的项目冷启动更快
  2. 热更新更快

那么是什么让它这么快?

Webpack 与 Vite 冷启动的区别

咱们先来看看 Webpack 与 Vite 的在构建上的区别。下图是 Webpack 的遍历递归收集依赖的过程:

上文咱们也讲了,Webpack 启动时,从入口文件登程,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都通过了本步骤的解决。

这一过程是十分十分耗时的,再看看 Vite:

Vite 通过在一开始将利用中的模块辨别为 依赖 源码 两类,改良了开发服务器启动工夫。它快的外围在于两点:

  1. 应用 Go 语言的依赖预构建:Vite 将会应用 esbuild 进行预构建依赖。esbuild 应用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。依赖预构建次要做了什么呢?

    • 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因而,Vite 必须先将作为 CommonJS 或 UMD 公布的依赖项转换为 ESM
    • Vite 将有许多外部模块的 ESM 依赖关系转换为单个模块,以进步后续页面加载性能。如果不编译,每个依赖包外面都可能含有多个其余的依赖,每个引入的依赖都会又一个申请,申请多了耗时就多
  2. 按需编译返回:Vite 以 原生 ESM 形式提供源码。这实际上是让浏览器接管了打包程序的局部工作:Vite 只须要在浏览器申请源码时进行转换并按需提供源码。依据情景动静导入代码,即只在以后屏幕上理论应用时才会被解决。

Webpack 与 Vite 热更新的区别

应用 Vite 的另外一个大的益处在于,它的热更新也是十分迅速的。

咱们首先来看看 Webpack 的热更新机制:

一些名词解释:

  • Webpack-complier:Webpack 的编译器,将 Javascript 编译成 bundle(就是最终的输入文件)
  • HMR Server:将热更新的文件输入给 HMR Runtime
  • Bunble Server:提供文件在浏览器的拜访,也就是咱们平时可能失常通过 localhost 拜访咱们本地网站的起因
  • HMR Runtime:开启了热更新的话,在打包阶段会被注入到浏览器中的 bundle.js,这样 bundle.js 就能够跟服务器建设连贯,通常是应用 Websocket,当收到服务器的更新指令的时候,就去更新文件的变动
  • bundle.js:构建输入的文件

Webpack 热更新的大抵原理是,文件通过 Webpack-complier 编译好后传输给 HMR Server,HMR Server 晓得哪个资源 (模块) 产生了扭转,并告诉 HMR Runtime 有哪些变动,HMR Runtime 就会更新咱们的代码,这样浏览器就会更新并且不须要刷新。

而 Webpack 热更新机制次要耗时点在于,Webpack 的热更新会以以后批改的文件为入口从新 build 打包,所有波及到的依赖也都会被从新加载一次

而 Vite 号称 热更新的速度不会随着模块增多而变慢。它的次要优化点在哪呢?

Vite 实现热更新的形式与 Webpack 大同小异,也通过创立 WebSocket 建设浏览器与服务器建设通信,通过监听文件的扭转向客户端收回音讯,客户端对应不同的文件进行不同的操作的更新。

Vite 通过 chokidar 来监听文件系统的变更,只用对产生变更的模块从新加载,只须要准确的使相干模块与其邻近的 HMR 边界连贯生效即可,这样 HMR 更新速度就不会因为利用体积的减少而变慢而 Webpack 还要经验一次打包构建。所以 HMR 场景下,Vite 体现也要好于 Webpack。

通过不同的音讯触发一些事件。做到浏览器端的即时热模块更换(热更新)。通过不同事件,触发更细粒度的更新(目前只有 Vue 和 JS,Vue 文件又蕴含了 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 我的项目革新,大抵只须要:

  1. 装置 Vite
  2. 配置 index.html(Vite 解析 <script type=”module” src=”…”> 标签指向源码)
  3. 配置 vite.config.js
  4. package.json 的 scripts 模块下减少启动命令 "vite": "vite"

当以命令行形式运行 npm run vite时,Vite 会主动解析我的项目根目录下名为 vite.config.js 的文件,读取相应配置。而对于 vite.config.js 的配置,整体而言比较简单:

  1. Vite 提供了对 .scss, .sass, .less, 和 .stylus 文件的内置反对
  2. 人造的对 TS 的反对,开箱即用
  3. 基于 Vue2 的我的项目反对,可能不同的我的项目会遇到不同的问题,依据报错逐渐调试即可,譬如通过一些官网插件兼容 .tsx.jsx

当然,对于我的项目的源码,可能须要肯定的革新,上面是咱们遇到的一些小问题:

  1. tsx 中应用装璜器导致的编译问题,咱们通过魔改了 @vitejs/plugin-vue-jsx,使其反对 Vue2 下的 jsx
  2. 因为 Vite 仅反对 ESM 语法,须要将代码中的模块引入形式由 require 改为 import
  3. Sass 预处理器无奈正确解析款式中的 /deep/,可应用 ::v-deep 替换
  4. 其余一些小问题,譬如 Webpack 环境变量的兼容,SVG iCON 的兼容

对于须要批改到源码的中央,咱们的做法是既保证能让 Vite 进行适配,同时让该改变不会影响到本来 Webpack 的构建,以便在关键时刻或者后续迭代能切回 Webpack

解决完上述的一些问题后,咱们胜利地将开发时基于 Webpack 的构建打包迁徙到了 Vite,成果也十分惊人,全模块构建耗时只有 2.6s

至此,开发阶段的构建耗时从本来的 4.5min 优化到了 2.6s:

构建模块数 耗时
Webpack 冷启动全量构建 20 个模块 4.5MIN
Webpack 冷启动只构建 1 个模块 18s
Webpack 有缓存状态下二次构建 1 个模块 4.5s
Vite 冷启动 2.6s

优化生产构建

好,上述咱们根本曾经实现了整个开发阶段的构建优化。下一步是 优化生产构建

咱们的生产公布是基于 GitLab 及 Jenkins 的残缺 CI/CD 流。

在优化之前,看看咱们的整个我的项目线上公布的耗时:

能够看到,生产环境构建工夫较长,build 均匀耗时约 9 分钟,整体公布构建时长在 15 分钟左右,整体构建环节耗时过长,效率低下,重大影响测试以及回滚

好,那咱们看看,整个构建流程,都须要做什么事件:

其中,Build baseBuild Region 阶段存在较大优化空间。

Build base 阶段的优化,波及到环境筹备,镜像拉取,依赖的装置。前端能施展的空间不大,这一块次要和 SRE 团队沟通,独特进行优化,能够做的有减少缓存解决、外挂文件系统、将依赖写进容器等形式。

咱们的优化,次要关注 Build Region 阶段,也就是外围关注如何缩小 npm run build 的工夫。

文章结尾有贴过 npm run build 的耗时剖析,简略再贴下:

<img width=”332″ alt=”image” src=”https://user-images.githubusercontent.com/8554143/172807307-d6b98ebd-c3dc-41d1-bc0f-1ef5ce2ebf68.png”>

一般而言,代码编译工夫和 代码规模 正相干。

依据以往优化教训,代码动态查看 可能会占据比拟多工夫,眼光锁定在 eslint-loader 上。

在生产构建阶段,eslint 提示信息价值不大,思考在 build 阶段去除,步骤前置

同时,咱们理解到,能够通过 esbuild-loader 插件去代替十分耗时的 babel-loader、ts-loader 等 loader。

因而,咱们的整体优化方向就是:

  1. 改写打包脚本,引入 esbuild 插件
  2. 优化构架逻辑,缩小 build 阶段不必要的查看

优化前后流程比照:

优化构架逻辑,缩小 build 阶段不必要的查看

这个下面说了,还是比拟好了解的,在生产构建阶段,eslint 提示信息价值不大,思考在 build 阶段去除,步骤前置

比方在 git commit 的时候利用 lint-stagedgit hook 做查看,或者利用 CI 在 git merge 的时候加一条流水线工作,专门做动态查看。

咱们两种形式都有做,简略给出接入 Gitlab CI 的代码:

// .gitlab-ci.yml
stages:
  - eslint

eslint-job:
  image: node:14.13.0
  stage: eslint
  script:
    - npm run lint 
    - echo 'eslint success'
  retry: 1
  rules:
    - if: '$CI_PIPELINE_SOURCE =="merge_request_event"&& $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =="test"'

通过 .gitlab-ci.yml 配置文件,指定固定的机会进行 lint 指令,前置步骤。

改写打包脚本,引入 esbuild 插件

这里,咱们次要借助了 esbuild-loader。

下面其实咱们也有提到 esbuild,Vite 应用 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-loader
    rule.uses.clear();

    // 增加 esbuild-loader
    rule
      .use('esbuild-loader')
      .loader('esbuild-loader')
      .options({
        loader: 'ts', // 如果应用了 ts, 或者 vue 的 class 装璜器,则须要加上这个 option 配置,否则会报错:ERROR: Unexpected "@"
        target: 'es2015',
        tsconfigRaw: require('./tsconfig.json')
      })

    // 删除底层 terser, 换用 esbuild-minimize-plugin
    config.optimization.minimizers.delete('terser');

    // 应用 esbuild 优化 css 压缩
    config.optimization
      .minimizer('esbuild')
      .use(ESBuildMinifyPlugin, [{ minify: true, css: true}]);
  }
}

移除 ESLint,以及接入 esbuild-loader 这一番组合拳打完,本地单次构建能够优化到 90 秒。

阶段 耗时
优化前 200s
移除 ESLint、接入 esbuild-loader 90s

再看看线上的 Jenkins 构建耗时,也有了一个非常明显的晋升:

前端工程化的演进及后续布局

整体而言,上述优化实现后,对整个我的项目的打包构建效率是有着一个比拟大的晋升的,然而这并非曾经做到了最好。

看看咱们旁边兄弟组的 Live 构建耗时:

在我的项目体量差不多的状况下,他们的生产构建耗时(npm run build)在 2 分钟出头,细究其原因在于:

  1. 他们的我的项目是 React + TSX,我这次优化的我的项目是 Vue,在文件的解决上就须要多过一层 vue-loader
  2. 他们的我的项目采纳了微前端,对我的项目对了拆分,主我的项目只须要加载基座相干的代码,子利用各自构建。须要构建的主利用代码量大大减少,这是次要起因;

是的,后续咱们还有许多能够尝试的方向,譬如咱们正在做的一些尝试有:

  1. 对我的项目进行微前端拆分,将绝对独立的模块拆解进去,做到独立部署
  2. 基于 Jenkinks 构建时,在 Build Base 阶段优化的晋升,譬如将构建流程前置,联合 CDN 做疾速回滚,以及将依赖预置进 Docker 容器中,缩小在容器中每次 npm install 工夫的耗费等

同时,咱们也必须看到,前端技术突飞猛进,各种构建工具目不暇给。前端从最晚期的刀耕火种,到逐渐向工程化迈进,到现在的泛前端工程化囊括的各式各样的规范、标准、各种提效的工具。构建效率优化可能会处于一种始终在路上的状态。当然,这里不肯定有最佳实际,只有最适宜咱们我的项目的实际,须要咱们一直地去摸索尝试。

最初

本文到此结束,心愿对你有帮忙 :)

如果还有什么疑难或者倡议,能够多多交换,原创文章,文笔无限,满腹经纶,文中若有不正之处,万望告知。

正文完
 0