冷啟動 4min -> 2s 的構(gòu)建優(yōu)化,怎么做到的潮模?(上篇)

項目背景

我們的系統(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ā)階段鳖宾,我們需要熱更新。

2逆航、基于生產(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 的 exclude 和 include 屬性辫呻。

  • 通過配置 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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末杉编,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子咆霜,更是在濱河造成了極大的恐慌邓馒,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛾坯,死亡現(xiàn)場離奇詭異光酣,居然都是意外死亡,警方通過查閱死者的電腦和手機脉课,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門救军,熙熙樓的掌柜王于貴愁眉苦臉地迎上來财异,“玉大人,你說我怎么就攤上這事缤言”Φ保” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵胆萧,是天一觀的道長庆揩。 經(jīng)常有香客問我,道長跌穗,這世上最難降的妖魔是什么订晌? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮蚌吸,結(jié)果婚禮上锈拨,老公的妹妹穿的比我還像新娘。我一直安慰自己羹唠,他們只是感情好奕枢,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著佩微,像睡著了一般缝彬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上哺眯,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天谷浅,我揣著相機與錄音,去河邊找鬼奶卓。 笑死一疯,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的夺姑。 我是一名探鬼主播墩邀,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瑟幕!你這毒婦竟也來了磕蒲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤只盹,失蹤者是張志新(化名)和其女友劉穎辣往,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體殖卑,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡站削,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了孵稽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片许起。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡十偶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出园细,到底是詐尸還是另有隱情惦积,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布猛频,位于F島的核電站狮崩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鹿寻。R本人自食惡果不足惜睦柴,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望毡熏。 院中可真熱鬧坦敌,春花似錦、人聲如沸痢法。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽财搁。三九已至训柴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妇拯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工洗鸵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留越锈,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓膘滨,卻偏偏與公主長得像甘凭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子火邓,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容