主頁 > 企業開發 > 前端構建效率優化之路

前端構建效率優化之路

2022-08-04 09:04:17 企業開發

專案背景

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

image

生產階段構建耗時

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

image

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

  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="https://www.cnblogs.com/coco1s/p/..."> 標簽指向原始碼)
  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 的耗時分析,簡單再貼下:

image

一般而言, 代碼編譯時間和代碼規模正相關,

根據以往優化經驗,代碼靜態檢查可能會占據比較多時間,目光鎖定在 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 時間的消耗等

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

最后

本文到此結束,希望對你有幫助 ??

如果還有什么疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/500819.html

標籤:JavaScript

上一篇:day 06 字串

下一篇:專案剛開始需要知道的東西

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more