前端構建效率優化之路

項目背景

我們的系統(一個 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。

生產階段構建耗時

而對于?npm run build,也就是實際線上生產環境的構建,看看總體的耗時:

因此,對于構建效率的優化可謂是勢在必行。首先,我們需要明確,優化分為兩個方向:

  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 的?exclude?和?include?屬性。

  • 通過配置 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 的熱更新機制:

  • 按需編譯返回:Vite 以?原生 ESM?方式提供源碼。這實際上是讓瀏覽器接管了打包程序的部分工作:Vite 只需要在瀏覽器請求源碼時進行轉換并按需提供源碼。根據情景動態導入代碼,即只在當前屏幕上實際使用時才會被處理。

一些名詞解釋:

  • 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.54min
Webpack 冷啟動只構建 1 個模塊18s
Webpack 有緩存狀態下二次構建 1 個模塊4.5s
Vite 冷啟動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。

因此,我們的整體優化方向就是:

  1. 改寫打包腳本,引入 esbuild 插件

  2. 優化構架邏輯,減少 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 success'retry: 1rules:- 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-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 分鐘出頭,細究其原因在于:

  1. 他們的項目是 React + TSX,我這次優化的項目是 Vue,在文件的處理上就需要多過一層?vue-loader

  2. 他們的項目采用了微前端,對項目對了拆分,主項目只需要加載基座相關的代碼,子應用各自構建。需要構建的主應用代碼量大大減少,這是主要原因;

是的,后續我們還有許多可以嘗試的方向,譬如我們正在做的一些嘗試有:

  1. 對項目進行微前端拆分,將相對獨立的模塊拆解出來,做到獨立部署

  2. 基于 Jenkinks 構建時,在?Build Base?階段優化的提升,譬如將構建流程前置,結合 CDN 做快速回滾,以及將依賴預置進 Docker 容器中,減少在容器中每次?npm install?時間的消耗等

同時,我們也必須看到,前端技術日新月異,各種構建工具目不暇給。前端從最早期的刀耕火種,到逐步向工程化邁進,到如今的泛前端工程化囊括的各式各樣的標準、規范、各種提效的工具。構建效率優化可能會處于一種一直在路上的狀態。當然,這里不一定有最佳實踐,只有最適合我們項目的實踐,需要我們不斷地去摸索嘗試。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/696360.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/696360.shtml
英文地址,請注明出處:http://en.pswp.cn/news/696360.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

ProtoBuf認識與Windows下的安裝

protobuf簡介 Protobuf 是 Protocol Buffers 的簡稱&#xff0c;它是 Google 公司開發的一種數據描述語言&#xff0c;是一種輕便高效的結 構化數據存儲格式&#xff0c;可以用于結構化數據&#xff0c;或者說序列化。它很適合做數據存儲 或 RPC 數據交換格 式 。可用于通訊…

WebServer -- 定時器處理非活動連接(上)

目錄 &#x1f34d;函數指針 &#x1f33c;基礎知識 &#x1f419;整體概述 &#x1f382;基礎API sigaction 結構體 sigaction() sigfillset() SIGALRM, SIGTERM 信號 alarm() socketpair() send() &#x1f4d5;信號通知流程 統一事件源 信號處理機制 &#x…

2024全球網絡安全展望|構建協同生態,護航數字經濟

2024年1月&#xff0c;世界經濟論壇發布《2024全球網絡安全展望》報告&#xff0c;指出在科技快速發展的背景下&#xff0c;網絡安全不均衡問題加劇&#xff0c;需加強公共部門、企業組織和個人的合作。 報告強調&#xff0c;面對地緣政治動蕩、技術不確定性和全球經濟波動&am…

基于springboot+vue的美發門店管理系統(前后端分離)

博主主頁&#xff1a;貓頭鷹源碼 博主簡介&#xff1a;Java領域優質創作者、CSDN博客專家、阿里云專家博主、公司架構師、全網粉絲5萬、專注Java技術領域和畢業設計項目實戰&#xff0c;歡迎高校老師\講師\同行交流合作 ?主要內容&#xff1a;畢業設計(Javaweb項目|小程序|Pyt…

Python 高級語法:一切皆對象

1 “一切皆對象”是一種核心設計哲學 在編程領域&#xff0c;特別是面向對象編程&#xff08;OOP&#xff09;中&#xff0c;“一切皆對象”是一種核心設計哲學。這種哲學主張&#xff0c;無論是數據、函數、還是更復雜的結構&#xff0c;都可以被視為對象&#xff0c;并賦予…

信息安全基本概念匯總

目錄 一、安全加密算法相關 二、信息安全需求規范相關 三、安全啟動 四、安全更新 五、安全通信SecOC 六、HSM安全固件整體架構 一、安全加密算法相關 基于Autosar的網絡安全理解_搜狐汽車_搜狐網 基于AES的CMAC算法、MAC、Hash、數字簽名之間的關系_aes cmac-CSDN博客…

Cartographer框架簡述

catographer框架分為前端和后端 前端包括雷達數據處理&#xff1b;位姿預測&#xff1b;掃描匹配和柵格地圖更新。 后端包括后端&#xff1a;線程池任務與調度&#xff1b;向位姿圖添加節點&#xff0c;計算節點的子圖內約束和子圖間約束&#xff08;回環檢測&#xff09;&…

C++之Easyx——圖形庫的基本功能(1):界面操作

最近&#xff0c;我覺得使用控制臺編寫游戲太沒意思了&#xff01;&#xff01; 所以我開始研究圖形庫了~ 一、setinitmode 函數定義 void EGEAPI setinitmode(int mode, int x CW_USEDEFAULT, int y CW_USEDEFAULT); //設置初始化模式&#xff0c;mode0為普通&#xff0c…

Spark中寫parquet文件是怎么實現的

背景 本文基于 Spark 3.5.0 寫本篇文章的目的是在于能夠配合spark.sql.maxConcurrentOutputFileWriters參數來加速寫parquet文件的速度&#xff0c;為此研究一下Spark寫parquet的時候會占用內存的大小&#xff0c;便于配置spark.sql.maxConcurrentOutputFileWriters的值&#…

Javascript怎么輸出內容?兩種常見方式以及控制臺介紹

javascript是一種非常重要的編程語言&#xff0c;在許多網頁中它被廣泛使用&#xff0c;可以實現許多交互效果和動態效果。輸出是javascript中最基本的操作之一&#xff0c;下面將介紹兩種常見的輸出方式。 一、使用console.log()函數輸出 console.log()函數是常用的輸出函數…

Jmeter實現階梯式線程增加的壓測

安裝相應jmeter 插件 1&#xff1a;安裝jmeter 管理插件&#xff1a; 下載地址&#xff1a;https://jmeter-plugins.org/install/Install/&#xff0c;將下載下來的jar包放到jmeter文件夾下的lib/ext路徑下&#xff0c;然后重啟jmeter。 2&#xff1a;接著打開 選項-Plugins Ma…

在Linux上安裝Docker: 一站式指南

Docker 是一款強大的容器化平臺&#xff0c;為開發者提供了一種輕松打包、發布和運行應用的方式。在本文中&#xff0c;我們將探討如何在Linux操作系統上安裝Docker&#xff0c;為你提供一站式指南。 步驟1: 卸載舊版本 在安裝新版Docker之前&#xff0c;建議先卸載舊版本&am…

三十年一個大輪回!日股突破“泡沫時期”歷史高點

2月22日周四&#xff0c;英偉達四季報業績超預期&#xff0c;而且本季度業績指引非常樂觀&#xff0c;提振美股股指期貨并成為芯片股和AI概念股情緒的重要催化劑。今日亞洲芯片股和AI股起飛&#xff0c;日本在芯片股的帶動下突破1989年泡沫時期以來的歷史最高收盤價。 美股方面…

我之前炒股虧麻了,找百融云AI Agent談了談心

春節之前&#xff0c;A股和H股都跌麻了&#xff0c;但是機構的路演和調研反而多了。因為&#xff1a;寫不完的安撫、說不完的陪伴、聽不完的客戶指責、以及撿不完的AH股便宜貨。 有一位血液里流淌著美式咖啡的職場白領&#xff0c;雖然這些年在股市過得很不如意&#xff0c;但…

C語言---鏈表

一.定義 鏈表是由一系列節點組成&#xff0c;每個結點包含兩個域&#xff0c;一個是數據域&#xff0c;數據域用來保存用戶數據&#xff0c;另一個是指針域&#xff0c;保存下一個節點的地址。鏈表在內存中是非連續的。 二.分類 靜態鏈表 動態鏈表 單向鏈表 雙向鏈表 循環鏈…

maven使用問題及解決辦法匯總

文章目錄 1、maven clean后打包出現Cannot create resource output directory2、把已有jar包打包進本地maven倉庫 1、maven clean后打包出現Cannot create resource output directory 主要原因是target目錄被別的程序占用了&#xff0c;最笨的辦法是重啟電腦&#xff0c;當然也…

C++跨模塊釋放內存

linux一個進程只有一個堆&#xff0c;不要考慮這些問題&#xff0c;但是windows一個進程可能有多個堆&#xff0c;要在對應的堆上釋放。 一&#xff0c; MT改MD 一個進程的地址空間是由一個可執行模塊和多個DLL模塊構成的&#xff0c;這些模塊中&#xff0c;有些可能會鏈接到…

代碼隨想錄訓練營第29天| 491.遞增子序列、46.全排列、47.全排列 II

491.遞增子序列 題目鏈接&#xff1a;491. 非遞減子序列 - 力扣&#xff08;LeetCode&#xff09; class Solution {List<List<Integer>> ans new ArrayList<>();public List<List<Integer>> findSubsequences(int[] nums) {backtrack(nums, …

(十三)【Jmeter】線程(Threads(Users))之tearDown 線程組

簡述 操作路徑如下: 作用:在正式測試結束后執行清理操作,如關閉連接、釋放資源等。配置:設置清理操作的采樣器、執行順序等參數。使用場景:確保在測試結束后應用程序恢復到正常狀態,避免資源泄漏或對其他測試的影響。優點:提供清理操作,確保測試環境的整潔和可重復性…

租用海外服務器,自己部署ChatGPT-Next-Web,實現ChatGPT聊天自由,還可以分享給朋友用

前言 如果有好幾個人需要使用ChatGPT&#xff0c;又沒有魔法上網環境&#xff0c;最好就是自己搭建一個海外的服務器環境&#xff0c;然后很多人就可以同時直接用了。 大概是情況是要花80元租一個一年的海外服務器&#xff0c;花15元租一個一年的域名&#xff0c;然后openai 的…