<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端構(gòu)建效率優(yōu)化之路

          共 22125字,需瀏覽 45分鐘

           ·

          2023-10-23 15:40

          項目背景

          我們的系統(tǒng)(一個 ToB 的 Web 單頁應(yīng)用)經(jīng)過多年的迭代,目前已經(jīng)累積有大幾十萬行的業(yè)務(wù)代碼,30+ 路由模塊,整體的代碼量和復(fù)雜度還是比較高的。

          項目整體是基于 Vue + TypeScirpt,而構(gòu)建工具,由于最早項目是經(jīng)由 vue-cli 初始化而來,所以自然而然使用的是 Webpack。

          我們知道,隨著項目體量越來越大,我們在開發(fā)階段將項目跑起來,也就是通過 npm run serve 的單次冷啟動時間,以及在項目發(fā)布時候的 npm run build 的耗時都會越來越久。

          因此,打包構(gòu)建優(yōu)化也是伴隨項目的成長需要持續(xù)不斷去做的事情。在早期,項目體量比較小的時,構(gòu)建優(yōu)化的效果可能還不太明顯,而隨著項目體量的增大,構(gòu)建耗時逐漸增加,如何盡可能的降低構(gòu)建時間,則顯得越來越重要:

          1. 大項目通常是團隊內(nèi)多人協(xié)同開發(fā),單次開發(fā)時的冷啟動時間的降低,乘上人數(shù)及天數(shù),經(jīng)年累月節(jié)省下來的時間非??捎^,能較大程度的提升開發(fā)效率、提升開發(fā)體驗
          2. 大項目的發(fā)布構(gòu)建的效率提升,能更好的保證項目發(fā)布、回滾等一系列操作的準確性、及時性

          本文,就將詳細介紹整個我們項目,在隨著項目體量不斷增大的過程中,對整體的打包構(gòu)建效率的優(yōu)化之路。

          瓶頸分析

          再更具體一點,我們的項目最初是基于 vue-cli 4,當時其基于的是 webpack4 版本。如無特殊說明,下文的一些配置會基于 webpack4 展開。

          工欲善其事必先利其器,解決問題前需要分析問題,要優(yōu)化構(gòu)建速度,首先得分析出 Webpack 構(gòu)建編譯我們的項目過程中,耗時所在,側(cè)重點分布。

          這里,我們使用的是 SMP 插件,統(tǒng)計各模塊耗時數(shù)據(jù)。

          speed-measure-webpack-plugin 是一款統(tǒng)計 webpack 打包時間的插件,不僅可以分析總的打包時間,還能分析各階段loader 的耗時,并且可以輸出一個文件用于永久化存儲數(shù)據(jù)。

          // 安裝
          npm install --save-dev speed-measure-webpack-plugin
          // 使用方式
          const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
          const smp = new SpeedMeasurePlugin();
          config.plugins.push(smp());

          開發(fā)階段構(gòu)建耗時

          對于 npm run serve,也就是開發(fā)階段而言,在沒有任何緩存的前提下,單次冷啟動整個項目的時間達到了驚人的 4 min。

          生產(chǎn)階段構(gòu)建耗時

          而對于 npm run build,也就是實際線上生產(chǎn)環(huán)境的構(gòu)建,看看總體的耗時:

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

          1. 基于開發(fā)階段 npm run serve 的優(yōu)化

          在開發(fā)階段,我們的核心目標是在保有項目所有功能的前提下,盡可能提高構(gòu)建速度,保證開發(fā)時的效率,所以對于 Live 才需要的一些功能,譬如代碼混淆壓縮、圖片壓縮等功能是可以不開啟的,并且在開發(fā)階段,我們需要熱更新。

          1. 基于生產(chǎn)階段 npm run build 的優(yōu)化

          而在生產(chǎn)打包階段,盡管構(gòu)建速度也非常重要,但是一些在開發(fā)時可有可無的功能必須加上,譬如代碼壓縮、圖片壓縮。因此,生產(chǎn)構(gòu)建的目標是在于保證最終項目打包體積盡可能小,所需要的相關(guān)功能盡可能完善的前提下,同時保有較快的構(gòu)建速度。

          兩者的目的不盡相同,因此一些構(gòu)建優(yōu)化手段可能僅在其中一個環(huán)節(jié)有效。

          基于上述的一些分析,本文將從如下幾個方面探討對構(gòu)建效率優(yōu)化的探索:

          1. 基于 Webpack 的一些常見傳統(tǒng)優(yōu)化方式
          2. 分模塊構(gòu)建
          3. 基于 Vite 的構(gòu)建工具切換
          4. 基于 Es-build 插件的構(gòu)建效率優(yōu)化

          為什么這么慢?

          那么,為什么隨著項目的增大,構(gòu)建的效率變得越來越慢了呢?

          從上面兩張截圖不難看出,對于我們這樣一個單頁應(yīng)用,構(gòu)建過程中的大部分時間都消耗在編譯 JavaScript 文件及 CSS 文件的各類  Loader 上。

          本文不會詳細描述 Webpack 的構(gòu)建原理,我們只需要大致知道,Webpack 的構(gòu)建流程,主要時間花費在遞歸遍歷各個入口文件,并基于入口文件不斷尋找依賴逐個編譯再遞歸處理的過程,每次遞歸都需要經(jīng)歷 String->AST->String 的流程,然后通過不同的 loader 處理一些字符串或者執(zhí)行一些 JavaScript 腳本,由于 NodeJS 單線程的特性以及語言本身的效率限制,Webpack 構(gòu)建慢一直成為它飽受詬病的原因。

          因此,基于上述 Webpack 構(gòu)建的流程及提到的一些問題,整體的優(yōu)化方向就變成了:

          1. 緩存
          2. 多進程
          3. 尋路優(yōu)化
          4. 抽離拆分
          5. 構(gòu)建工具替換

          基于 Webpack 的傳統(tǒng)優(yōu)化方式

          上面也說了,構(gòu)建過程中的大部分時間都消耗在遞歸地去編譯 JavaScript 及 CSS 的各類  Loader 上,并且會受限于 NodeJS 單線程的特性以及語言本身的效率限制。

          如果不替換掉 Webpack 本身,語言本身(NodeJS)的執(zhí)行效率是沒法優(yōu)化的,只能在其他幾個點做文章。

          因此在最早期,我們所做的都是一些比較常規(guī)的優(yōu)化手段,這里簡單介紹最為核心的幾個:

          1. 緩存
          2. 多進程
          3. 尋址優(yōu)化

          緩存優(yōu)化

          其實對于 vue-cli 4 而言,已經(jīng)內(nèi)置了一些緩存操作,譬如上圖可見到 loader 的過程中,有使用 cache-loader,所以我們并不需要再次添加到項目之中。

          • cache-loader: 在一些性能開銷較大的 loader 之前添加 cache-loader,以便將結(jié)果緩存到磁盤里

          那還有沒有一些其他的緩存操作呢用上的呢?我們使用了一個 HardSourceWebpackPlugin 。

          HardSourceWebpackPlugin

          • HardSourceWebpackPlugin: HardSourceWebpackPlugin 為模塊提供中間緩存,緩存默認存放的路徑是 node_modules/.cache/hard-source,配置了 HardSourceWebpackPlugin 之后,首次構(gòu)建時間并沒有太大的變化,但是第二次開始,構(gòu)建時間將會大大的加快。

          首先安裝依賴:

          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 的首次構(gòu)建時間,和預(yù)期的一樣,并沒有太大的變化,但是第二次構(gòu)建從平均 4min 左右降到了平均 20s 左右,提升的幅度非常的夸張,當然,這個也因項目而異,但是整體而言,在不同項目中實測發(fā)現(xiàn)它都能比較大的提升開發(fā)時二次編譯的效率。

          設(shè)置 babel-loader 的 cacheDirectory 以及 DLL

          另外,在緩存方面我們的嘗試有:

          1. 設(shè)置 babel-loader 的 cacheDirectory
          2. DLL

          但是整體收效都不太大,可以簡單講講。

          打開 babel-loader 的 cacheDirectory 的配置,當有設(shè)置時,指定的目錄將用來緩存 loader 的執(zhí)行結(jié)果。之后的 webpack 構(gòu)建,將會嘗試讀取緩存,來避免在每次執(zhí)行時,可能產(chǎn)生的、高性能消耗的 Babel 重新編譯過程。實際的操作步驟,你可以看看 Webpack - babel-loader。

          那么 DLL 又是什么呢?

          DLL 文件為動態(tài)鏈接庫,在一個動態(tài)鏈接庫中可以包含給其他模塊調(diào)用的函數(shù)和數(shù)據(jù)。

          為什么要用 DLL?

          原因在于包含大量復(fù)用模塊的動態(tài)鏈接庫只需要編譯一次,在之后的構(gòu)建過程中被動態(tài)鏈接庫包含的模塊將不會在重新編譯,而是直接使用動態(tài)鏈接庫中的代碼。

          由于動態(tài)鏈接庫中大多數(shù)包含的是常用的第三方模塊,例如 Vue、React、React-dom,只要不升級這些模塊的版本,動態(tài)鏈接庫就不用重新編譯。

          DLL 的配置非常繁瑣,并且最終收效甚微,我們在過程中借助了 autodll-webpack-plugin,感興趣的可以自行嘗試。值得一提的是,Vue-cli 已經(jīng)剔除了這個功能。

          多進程

          基于 NodeJS 單線程的特性,當有多個任務(wù)同時存在,它們也只能排隊串行執(zhí)行。

          而如今大多數(shù) CPU 都是多核的,因此我們可以借助一些工具,充分釋放 CPU 在多核并發(fā)方面的優(yōu)勢,利用多核優(yōu)勢,多進程同時處理任務(wù)。

          從上圖中可以看到,Vue CLi4 中,其實已經(jīng)內(nèi)置了 thread-loader

          • thread-loader: 把 thread-loader 放置在其它 loader 之前,那么放置在這個 loader 之后的 loader 就會在一個單獨的 worker 池中運行。這樣做的好處是把原本需要串行執(zhí)行的任務(wù)并行執(zhí)行。

          那么,除了 thread-loader,還有哪些可以考慮的方案呢?

          HappyPack

          HappyPack 與 thread-loader 類似。

          HappyPack 可利用多進程對文件進行打包, 將任務(wù)分解給多個子進程去并行執(zhí)行,子進程處理完后,再把結(jié)果發(fā)送給主進程,達到并行打包的效、HappyPack 并是所有的 loader 都支持, 比如 vue-loader 就不支持。

          可以通過 Loader Compatibility List 來查看支持的 loaders。需要注意的是,創(chuàng)建子進程和主進程之間的通信是有開銷的,當你的 loader 很慢的時候,可以加上 happypack。否則,可能會編譯的更慢。

          當然,由于 HappyPack 作者對 JavaScript 的興趣逐步丟失,維護變少,webpack4 及之后都更推薦使用 thread-loader。因此,這里沒有實際結(jié)論給出。

          上一次 HappyPack 更新已經(jīng)是 3 年前

          尋址優(yōu)化

          對于尋址優(yōu)化,總體而言提升并不是很大。

          它的核心即在于,合理設(shè)置 loader 的 excludeinclude 屬性。

          • 通過配置 loader 的 exclude 選項,告訴對應(yīng)的 loader 可以忽略某個目錄
          • 通過配置 loader 的 include 選項,告訴 loader 只需要處理指定的目錄,loader 處理的文件越少,執(zhí)行速度就會更快

          這肯定是有用的優(yōu)化手段,只是對于一些大型項目而言,這類優(yōu)化對整體構(gòu)建時間的優(yōu)化不會特別明顯。

          分模塊構(gòu)建

          在上述的一些常規(guī)優(yōu)化完成后。整體效果仍舊不是特別明顯,因此,我們開始思考一些其它方向。

          我們再來看看 Webpack 構(gòu)建的整體流程:

          上圖是大致的 webpack 構(gòu)建流程,簡單介紹一下:

          1. entry-option:讀取 webpack 配置,調(diào)用 new Compile(config) 函數(shù)準備編譯
          2. run:開始編譯
          3. make:從入口開始分析依賴,對依賴模塊進行 build
          4. before-resolve:對位置模塊進行解析
          5. build-module:開始構(gòu)建模塊
          6. normal-module-loader:生成 AST 樹
          7. program:遍歷 AST 樹,遇到 require 語句收集依賴
          8. seal:build 完成開始優(yōu)化
          9. emit:輸出 dist 目錄

          隨著項目體量地不斷增大,耗時大頭消耗在第 7 步,遞歸遍歷 AST,解析 require,如此反復(fù)直到遍歷完整個項目。

          而有意思的是,對于單次單個開發(fā)而言,極大概率只是基于這整個大項目的某一小個模塊進行開發(fā)即可。

          所以,如果我們可以在收集依賴的時候,跳過我們本次不需要的模塊,或者可以自行選擇,只構(gòu)建必要的模塊,那么整體的構(gòu)建時間就可以大大減少。

          這也就是我們要做的 -- 分模塊構(gòu)建。

          什么意思呢?舉個栗子,假設(shè)我們的項目一共有 6 個大的路由模塊 A、B、C、D、E、F,當新需求只需要在 A 模塊范圍內(nèi)進行優(yōu)化新增,那么我們在開發(fā)階段啟動整個項目的時候,可以跳過 B、C、D、E、F 這 5 個模塊,只構(gòu)建 A 模塊即可:

          假設(shè)原本每個模塊的構(gòu)建平均耗時 3s,原本 18s 的整體冷啟動構(gòu)建耗時就能下降到 3s。

          分模塊構(gòu)建打包的原理

          Webpack 是靜態(tài)編譯打包的,Webpack 在收集依賴時會去分析代碼中的 require(import 會被 bebel 編譯成 require) 語句,然后遞歸的去收集依賴進行打包構(gòu)建。

          我們要做的,就是通過增加一些配置,簡單改造下我們的現(xiàn)有代碼,使得 Webpack 在初始化遍歷整個路由模塊收集依賴的時候,可以跳過我們不需要的模塊。

          再說得詳細點,假設(shè)我們的路由大致代碼如下:

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

          // 1. 定義路由組件.
          // 這里簡化下模型,實際項目中肯定是一個一個的大路由模塊,從其他文件導(dǎo)入
          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 配置,無需改變?nèi)魏温酚晌募a。總結(jié)而言,該方案的兩點優(yōu)勢在于:

          1. 無需改動上層代碼
          2. 通過生成臨時路由文件的方式,替換原路由文件,對項目無任何影響

          使用 NormalModuleReplacementPlugin 生成新的路由配置文件

          利用 NormalModuleReplacementPlugin 插件,可以不修改原來的路由配置文件,在編譯階段根據(jù)配置生成一個新的路由配置文件然后去使用它,這樣做的好處在于對整個源碼沒有侵入性。

          NormalModuleReplacementPlugin 插件的作用在于,將目標源文件的內(nèi)容替換為我們自己的內(nèi)容。

          我們簡單修改 Webpack 配置,如果當前是開發(fā)環(huán)境,利用該插件,將原本的 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 文件的內(nèi)容又是如何產(chǎn)生的呢,其實就是借助了 inquirer 與 EJS 模板引擎,通過一個交互式的命令行問答,選取需要的模塊,基于選擇的內(nèi)容,動態(tài)的生成新的 dev.routerConfig.ts 代碼,這里直接上代碼。

          改造一下我們的啟動腳本,在執(zhí)行 vue-cli-service serve 前,先跑一段我們的前置腳本:

          {
          // ...
          "scripts": {
          - "dev": "vue-cli-service serve",
          + "dev": "node ./script/dev-server.js && vue-cli-service serve",
          },
          // ...
          }

          dev-server.js 所需要做的事,就是通過 inquirer 實現(xiàn)一個交互式命令,用戶選擇本次需要啟動的模塊列表,通過 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',
          // 實際業(yè)務(wù)中的所有模塊
          ]

          //選中的模塊
          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 交互命令行服務(wù),讓用戶選擇需要構(gòu)建的模塊,類似于這樣:

          模板代碼示意 router.config.template.ejs 是 EJS 模板文件,chooseModules 是我們在終端輸入時,獲取到的用戶選擇的模塊集合數(shù)組,根據(jù)這個列表,我們?nèi)ド尚碌?routesConfig 文件。

          這樣,我們就實現(xiàn)了分模塊構(gòu)建,按需進行依賴收集。以我們的項目為例,我們的整個項目大概有 20 個不同的模塊,幾十萬行代碼:

          構(gòu)建模塊數(shù) 耗時
          冷啟動全量構(gòu)建 20 個模塊 4.5MIN
          冷啟動只構(gòu)建 1 個模塊 18s
          有緩存狀態(tài)下二次構(gòu)建 1 個模塊 4.5s

          實際效果大致如下,無需啟動所有模塊,只啟動我們選中的模塊進行對應(yīng)的開發(fā)即可:

          這樣,如果單次開發(fā)只涉及固定的模塊,單次項目冷啟動的時間,可以從原本的 4min+ 下降到 18s 左右,而有緩存狀態(tài)下二次構(gòu)建 1 個模塊,僅僅需要 4.5s,屬于一個比較大的提升。

          受限于 Webpack 所使用的語言的性能瓶頸,要追求更快的構(gòu)建性能,我們不可避免的需要把目光放在其他構(gòu)建工具上。這里,我們的目光聚焦在了 Vite 與 esbuild 上。

          使用 Vite 優(yōu)化開發(fā)時構(gòu)建

          Vite,一個基于瀏覽器原生 ES 模塊的開發(fā)服務(wù)器。利用瀏覽器去解析 imports,在服務(wù)器端按需編譯返回,完全跳過了打包這個概念,服務(wù)器隨起隨用。同時不僅有 Vue 文件支持,還搞定了熱更新,而且熱更新的速度不會隨著模塊增多而變慢。

          當然,由于 Vite 本身特性的限制,目前只適用于在開發(fā)階段替代 Webpack。

          我們都知道 Vite 非??欤饕煸谑裁吹胤??

          1. 項目冷啟動更快
          2. 熱更新更快

          那么是什么讓它這么快?

          Webpack 與 Vite 冷啟動的區(qū)別

          我們先來看看 Webpack 與 Vite 的在構(gòu)建上的區(qū)別。下圖是 Webpack 的遍歷遞歸收集依賴的過程:

          上文我們也講了,Webpack 啟動時,從入口文件出發(fā),調(diào)用所有配置的 Loader 對模塊進行編譯,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經(jīng)過了本步驟的處理。

          這一過程是非常非常耗時的,再看看 Vite:

          Vite 通過在一開始將應(yīng)用中的模塊區(qū)分為 依賴源碼 兩類,改進了開發(fā)服務(wù)器啟動時間。它快的核心在于兩點:

          1. 使用 Go 語言的依賴預(yù)構(gòu)建:Vite 將會使用 esbuild 進行預(yù)構(gòu)建依賴。esbuild 使用 Go 編寫,并且比以 JavaScript 編寫的打包器預(yù)構(gòu)建依賴快 10-100 倍。依賴預(yù)構(gòu)建主要做了什么呢?

            • 開發(fā)階段中,Vite 的開發(fā)服務(wù)器將所有代碼視為原生 ES 模塊。因此,Vite 必須先將作為 CommonJS 或 UMD 發(fā)布的依賴項轉(zhuǎn)換為 ESM
            • Vite 將有許多內(nèi)部模塊的 ESM 依賴關(guān)系轉(zhuǎn)換為單個模塊,以提高后續(xù)頁面加載性能。如果不編譯,每個依賴包里面都可能含有多個其他的依賴,每個引入的依賴都會又一個請求,請求多了耗時就多
          2. 按需編譯返回:Vite 以 原生 ESM 方式提供源碼。這實際上是讓瀏覽器接管了打包程序的部分工作:Vite 只需要在瀏覽器請求源碼時進行轉(zhuǎn)換并按需提供源碼。根據(jù)情景動態(tài)導(dǎo)入代碼,即只在當前屏幕上實際使用時才會被處理。

          Webpack 與 Vite 熱更新的區(qū)別

          使用 Vite 的另外一個大的好處在于,它的熱更新也是非常迅速的。

          我們首先來看看 Webpack 的熱更新機制:

          一些名詞解釋:

          • Webpack-complier:Webpack 的編譯器,將 Javascript 編譯成 bundle(就是最終的輸出文件)
          • HMR Server:將熱更新的文件輸出給 HMR Runtime
          • Bunble Server:提供文件在瀏覽器的訪問,也就是我們平時能夠正常通過 localhost 訪問我們本地網(wǎng)站的原因
          • HMR Runtime:開啟了熱更新的話,在打包階段會被注入到瀏覽器中的 bundle.js,這樣 bundle.js 就可以跟服務(wù)器建立連接,通常是使用 Websocket ,當收到服務(wù)器的更新指令的時候,就去更新文件的變化
          • bundle.js:構(gòu)建輸出的文件

          Webpack 熱更新的大致原理是,文件經(jīng)過 Webpack-complier 編譯好后傳輸給 HMR Server,HMR Server 知道哪個資源 (模塊) 發(fā)生了改變,并通知 HMR Runtime 有哪些變化,HMR Runtime 就會更新我們的代碼,這樣瀏覽器就會更新并且不需要刷新。

          而 Webpack 熱更新機制主要耗時點在于,Webpack 的熱更新會以當前修改的文件為入口重新 build 打包,所有涉及到的依賴也都會被重新加載一次。

          而  Vite 號稱 熱更新的速度不會隨著模塊增多而變慢。它的主要優(yōu)化點在哪呢?

          Vite 實現(xiàn)熱更新的方式與 Webpack 大同小異,也通過創(chuàng)建 WebSocket 建立瀏覽器與服務(wù)器建立通信,通過監(jiān)聽文件的改變向客戶端發(fā)出消息,客戶端對應(yīng)不同的文件進行不同的操作的更新。

          Vite 通過 chokidar 來監(jiān)聽文件系統(tǒng)的變更,只用對發(fā)生變更的模塊重新加載,只需要精確的使相關(guān)模塊與其臨近的 HMR 邊界連接失效即可,這樣 HMR 更新速度就不會因為應(yīng)用體積的增加而變慢而 Webpack 還要經(jīng)歷一次打包構(gòu)建。所以 HMR 場景下,Vite 表現(xiàn)也要好于 Webpack。

          通過不同的消息觸發(fā)一些事件。做到瀏覽器端的即時熱模塊更換(熱更新)。通過不同事件,觸發(fā)更細粒度的更新(目前只有 Vue 和 JS,Vue 文件又包含了 template、script、style 的改動),做到只更新必須的文件,而不是全量進行更新。在些事件分別是:

          • connected: WebSocket 連接成功
          • vue-reload: Vue 組件重新加載(當修改了 script 里的內(nèi)容時)
          • vue-rerender: Vue 組件重新渲染(當修改了 template 里的內(nèi)容時)
          • style-update: 樣式更新
          • style-remove: 樣式移除
          • js-update: js 文件更新
          • full-reload: fallback 機制,網(wǎng)頁重刷新

          本文不會在 Vite 原理上做太多深入,感興趣的可以通過官方文檔了解更多 -- Vite 官方文檔 -- 為什么選 Vite

          基于 Vite 的改造,相當于在開發(fā)階段替換掉 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 的文件,讀取相應(yīng)配置。而對于 vite.config.js 的配置,整體而言比較簡單:

          1. Vite 提供了對 .scss, .sass, .less, 和 .stylus 文件的內(nèi)置支持
          2. 天然的對 TS 的支持,開箱即用
          3. 基于 Vue2 的項目支持,可能不同的項目會遇到不同的問題,根據(jù)報錯逐步調(diào)試即可,譬如通過一些官方插件兼容 .tsx、.jsx

          當然,對于項目的源碼,可能需要一定的改造,下面是我們遇到的一些小問題:

          1. tsx 中使用裝飾器導(dǎo)致的編譯問題,我們通過魔改了 @vitejs/plugin-vue-jsx,使其支持 Vue2 下的 jsx
          2. 由于 Vite 僅支持 ESM 語法,需要將代碼中的模塊引入方式由 require 改為 import
          3. Sass 預(yù)處理器無法正確解析樣式中的 /deep/,可使用 ::v-deep 替換
          4. 其他一些小問題,譬如 Webpack 環(huán)境變量的兼容,SVG iCON 的兼容

          對于需要修改到源碼的地方,我們的做法是既保證能讓 Vite 進行適配,同時讓該改動不會影響到原本 Webpack 的構(gòu)建,以便在關(guān)鍵時刻或者后續(xù)迭代能切回 Webpack

          解決完上述的一些問題后,我們成功地將開發(fā)時基于 Webpack 的構(gòu)建打包遷移到了 Vite,效果也非常驚人,全模塊構(gòu)建耗時只有 2.6s

          至此,開發(fā)階段的構(gòu)建耗時從原本的 4.5min 優(yōu)化到了 2.6s:

          構(gòu)建模塊數(shù) 耗時
          Webpack 冷啟動全量構(gòu)建 20 個模塊 4.5MIN
          Webpack 冷啟動只構(gòu)建 1 個模塊 18s
          Webpack 有緩存狀態(tài)下二次構(gòu)建 1 個模塊 4.5s
          Vite 冷啟動 2.6s

          優(yōu)化生產(chǎn)構(gòu)建

          好,上述我們基本已經(jīng)完成了整個開發(fā)階段的構(gòu)建優(yōu)化。下一步是優(yōu)化生產(chǎn)構(gòu)建。

          我們的生產(chǎn)發(fā)布是基于 GitLab 及 Jenkins 的完整 CI/CD 流。

          在優(yōu)化之前,看看我們的整個項目線上發(fā)布的耗時:

          可以看到,生產(chǎn)環(huán)境構(gòu)建時間較長, build 平均耗時約 9 分鐘,整體發(fā)布構(gòu)建時長在 15 分鐘左右,整體構(gòu)建環(huán)節(jié)耗時過長, 效率低下,嚴重影響測試以及回滾

          好,那我們看看,整個構(gòu)建流程,都需要做什么事情:

          其中, Build baseBuild Region 階段存在較大優(yōu)化空間。

          Build base 階段的優(yōu)化,涉及到環(huán)境準備,鏡像拉取,依賴的安裝。前端能發(fā)揮的空間不大,這一塊主要和 SRE 團隊溝通,共同進行優(yōu)化,可以做的有增加緩存處理、外掛文件系統(tǒng)、將依賴寫進容器等方式。

          我們的優(yōu)化,主要關(guān)注 Build Region 階段,也就是核心關(guān)注如何減少 npm run build 的時間。

          文章開頭有貼過 npm run build 的耗時分析,簡單再貼下:

          一般而言, 代碼編譯時間和代碼規(guī)模正相關(guān)。

          根據(jù)以往優(yōu)化經(jīng)驗,代碼靜態(tài)檢查可能會占據(jù)比較多時間,目光鎖定在 eslint-loader 上。

          在生產(chǎn)構(gòu)建階段,eslint 提示信息價值不大,考慮在 build 階段去除,步驟前置。

          同時,我們了解到,可以通過 esbuild-loader 插件去替代非常耗時的 babel-loader、ts-loader 等 loader。

          因此,我們的整體優(yōu)化方向就是:

          1. 改寫打包腳本,引入 esbuild 插件
          2. 優(yōu)化構(gòu)架邏輯,減少 build 階段不必要的檢查

          優(yōu)化前后流程對比:

          優(yōu)化構(gòu)架邏輯,減少 build 階段不必要的檢查

          這個上面說了,還是比較好理解的,在生產(chǎn)構(gòu)建階段,eslint 提示信息價值不大,考慮在 build 階段去除,步驟前置。

          比如在 git commit 的時候利用 lint-stagedgit hook 做檢查, 或者利用 CI 在 git merge 的時候加一條流水線任務(wù),專門做靜態(tài)檢查。

          我們兩種方式都有做,簡單給出接入 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 進行預(yù)構(gòu)建依賴。這里我們借助的是 esbuild-loader,它把 esbuild 的能力包裝成 Webpack 的 loader 來實現(xiàn) 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',
                  tsconfigRawrequire('./tsconfig.json')
                })

              // 刪除底層 terser, 換用 esbuild-minimize-plugin
              config.optimization.minimizers.delete('terser');

              // 使用 esbuild 優(yōu)化 css 壓縮
              config.optimization
                .minimizer('esbuild')
                .use(ESBuildMinifyPlugin, [{ minifytruecsstrue }]);
            }
          }

          移除 ESLint,以及接入 esbuild-loader 這一番組合拳打完,本地單次構(gòu)建可以優(yōu)化到 90 秒。

          階段 耗時
          優(yōu)化前 200s
          移除 ESLint、接入 esbuild-loader 90s

          再看看線上的 Jenkins 構(gòu)建耗時,也有了一個非常明顯的提升:

          前端工程化的演進及后續(xù)規(guī)劃

          整體而言,上述優(yōu)化完成后,對整個項目的打包構(gòu)建效率是有著一個比較大的提升的,但是這并非已經(jīng)做到了最好。

          看看我們旁邊兄弟組的 Live 構(gòu)建耗時:

          在項目體量差不多的情況下,他們的生產(chǎn)構(gòu)建耗時(npm run build)在 2 分鐘出頭,細究其原因在于:

          1. 他們的項目是 React + TSX,我這次優(yōu)化的項目是 Vue,在文件的處理上就需要多過一層 vue-loader;
          2. 他們的項目采用了微前端,對項目對了拆分,主項目只需要加載基座相關(guān)的代碼,子應(yīng)用各自構(gòu)建。需要構(gòu)建的主應(yīng)用代碼量大大減少,這是主要原因;

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

          1. 對項目進行微前端拆分,將相對獨立的模塊拆解出來,做到獨立部署
          2. 基于 Jenkinks 構(gòu)建時,在 Build Base 階段優(yōu)化的提升,譬如將構(gòu)建流程前置,結(jié)合 CDN 做快速回滾,以及將依賴預(yù)置進 Docker 容器中,減少在容器中每次 npm install 時間的消耗等

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

          瀏覽 935
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日皮视频免费看 | 岛国乱婬A片免费看 | 欧美xxxx操 | 福利一区在线观看 | 男人天堂婷婷 |