Vite大摸底!凭什么它这么快?

388 阅读17分钟

上面的图当然就开开玩笑~vite经过这几年的发展是一路高歌,越来越被广大用户喜欢,而它的热度在GitHub上即将突破7W大关了,相比我们的老大哥webpack是稍微快了一点点。。。

当然更重要的是打开vite的工程,你看它运行速度很快,实际上就是很快~~

这就不得不让我们思考为什么vite它这么快呢?

1.工具的发展历程

谈到vite快的理由,我们不得不追溯到“远古时代”,工具发展的“进化论”。早在多年以前,大概是2000年左右,那时候的人写代码面临一个痛点,声明的变量和函数,在命名空间里面,非常容易发生冲突,如:

var globalVar = "I am global";
function globalFunction() {
  console.log(globalVar);
}

全局的命名空间会经常发生命名冲突,让写代码的人非常烦恼,特别是维护大型的项目,简直是“如履薄冰”!于是乎模块化的这个词慢慢的在人们心中萌芽,中期阶段我们来到了简单的模块化方案,如:

(function() {
  var localVar = "I am local";
  function localFunction() {
    console.log(localVar);
  }
  localFunction();
})();

终于我们诞生了创建独立作用域,避免全局污染的方法。可这个方法依旧存在缺点,不同的模块加载顺序不一样会对代码有所影响,我们开始更多的思考是否有更加合理的办法,随着node诞生于发展,我们来到了CommonJS时代...

// math.js
exports.add = function(a, b) {
  return a + b;
};

// app.js
const math = require('./math');
console.log(math.add(2, 3)); // 输出: 5

慢慢的我们有了CommonJs的规范,但它主要还是用于Node.js环境,更多的是适用于服务器端的开发,对于浏览器端,我们又陷入了模块思考的路子里面。。。

// math.js
define(function() {
  return {
    add: function(a, b) {
      return a + b;
    }
  };
});

// app.js
require(['math'], function(math) {
  console.log(math.add(2, 3)); // 输出: 5
});

此时AMD规范应运而生,当时的require.js名声响亮,异常火爆,甚至至今你都可以看到有许多项目还是有require.js的身影。

时代是不断进步的,长江后浪推前浪,ES6 Module横空出世, 它被称作 ES Module(或 ESM), 是由 ECMAScript 官方提出的模块化规范,作为一个官方提出的规范。它如新皇诞生,赢得了众多的浏览器支持,如今所向披靡。

不仅如此,一直以 CommonJS 作为模块标准的 Node.js 也紧跟 ES Module 的发展步伐,从 12.20 版本开始正式支持原生 ES Module。也就是说,如今 ES Module 能够同时在浏览器与 Node.js 环境中执行,拥有天然的跨平台能力。

为什么会谈到模块化这个呢?因为Vite与 ES 模块(ESM)之间的关系紧密且互补,可以说ESM可以没有Vite,但Vite绝对不能失去ESM,非常复杂,且听我娓娓道来。。。

2.Vite项目初始化

npx degit user/project#main my-project
cd my-project

npm install
npm run dev

这一段代码几乎学习过Vite的都知道,在我们Vite官网里面,每一个初学者基本都必经成长的路。

在启动服务,到浏览器上面展示的时候,我们打开浏览器的工具栏,点开请求你会发现

你会发现浏览器解析的时候,只要是你文件里面import进来的包都会变成HTTP的请求,这是为什么呢?其实这就是ESM的魅力。

<script type="module" src="/src/main.js"></script>

当声明一个 script标签类型为 module 时,浏览器解析资源就会往当前域名发起一个GET请求main.js文件,而检测到内部含有import引入的包,就会import 引用发起HTTP请求获取模块的内容文件,如App.vue、vue文件等。

同时,Vite 还利用HTTP加速整个页面的重新加载。设置响应头使得依赖模块(dependency module)进行强缓存,而源码文件通过设置 304 Not Modified 而变成可依据条件而进行更新。

骚年们,Vite快的核心一面已经展现出来了,为什么它那么快,就因为ESM的强大。

其核心原理是利用浏览器现在已经支持ES6的import,碰见import就会发送一个HTTP请求去加载文件,Vite启动一个 koa 服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以ESM格式返回返回给浏览器。Vite整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度快许多!

3. 基于ESM的Dev server

这不禁又让我们想到了我们的老大哥Webpack...

当 webpack 处理应用程序时,它会根据命令行参数中或配置文件中定义的模块列表开始处理。 从 入口 开始,webpack 会递归的构建一个 依赖关系图,这个依赖图包含着应用程序中所需的每个模块,然后将所有模块打包为少量的 bundle —— 通常只有一个 —— 可由浏览器加载。但当项目应用越大,启动时间越长。

而我们的Vite则是利用浏览器对ESM的支持,当 import 模块时,浏览器就会下载被导入的模块。先启动开发服务器,当代码执行到模块加载时再请求对应模块的文件,import() 语法来实现模块的动态加载。灰色部分是暂时没有用到的路由,所有这部分不会参与构建过程。随着项目里的应用越来越多,增加route,也不会影响其构建速度。

4.基于ESM 的 HMR 热更新

这个时候Vite又再次“亮剑”HMR,首先我们看看“亮剑”的本钱

HMR:一种在不重新加载整个页面的情况下更新应用的技术。它允许只替换或添加已更改的模块,而不会影响应用程序的状态。 

WebSocket:Vite 使用 WebSocket 来建立客户端与服务器之间的实时通信通道,用于传递文件变更信息。

ESM (ECMAScript Modules):现代 JavaScript 模块系统,支持动态导入 import() 和其他先进的模块功能。

拥有这三大打手的情况下,我们的HMR基本是打遍天下无敌手了

vite热更新流程

启动开发服务器:当启动 Vite 开发服务器时,它会监听项目文件的变化,并准备好通过 WebSocket 发送更新给浏览器端。 

创建 WebSocket 连接:浏览器中的 Vite 客户端脚本会尝试与开发服务器建立 WebSocket 连接。一旦连接成功,就保持开放状态以便接收来自服务器的消息。 

文件变更检测:开发服务器持续监控项目中的文件变化。每当检测到文件被修改、新增或删除时,它都会触发相应的事件。 

发送更新通知:对于每个文件变更,开发服务器会通过 WebSocket 向所有连接的客户端发送一条消息,包含有关哪些模块需要更新的信息。 

客户端处理更新: 收到更新通知后,客户端首先检查是否可以进行热更新(即该模块是否支持 HMR)。 如果支持,那么客户端会使用 import.meta.hot API 来接受新的模块内容并替换旧的内容。 对于 CSS 文件,直接替换样式表;对于 JavaScript 文件,则可能涉及更复杂的逻辑,如重新执行部分函数或重新渲染组件等。 如果不支持 HMR 或者更新失败,Vite 会回退到完全页面刷新。 

自定义 HMR 更新逻辑:开发者可以通过注册 import.meta.hot.accept 回调来定义特定模块如何响应更新。

用一张图简单的展示下:

这是只是宏观对对Vite热更新概念的理解,接下来我们来走进“微观”的源码里面看。

5.Vite热更新原理解析

经过上面的说明,我们对热更新有一个初步的印象。就像追女仔,印象分已经给到了。接下来就看行动了

源码地址:packages/vite/src/node/server/index.ts

既然创建服务有了,那接下来我们就要进行相应的文件监听,这个时候又得找另外一个源码的类moduleGraph。

moduleGraph

moduleGraph辅助整个模块的依赖关系收集,可以把它想象成一个图书馆一样,不断的对文件进行依赖跟踪、路径收集、依赖解析、更新处理等。

它实际上做的事情如下:

模块依赖追踪:moduleGraph 跟踪项目中所有模块之间的依赖关系。这包括入口文件、JavaScript 模块、CSS 文件等。 

支持 HMR:通过维护模块间的依赖关系,moduleGraph 可以准确地识别出哪些模块需要在代码发生变化时进行更新,并且能够正确地传播这些更新。

资源缓存与复用:它帮助 Vite 在开发过程中缓存已处理过的模块,从而避免重复处理相同的模块,提高性能。 

错误处理:记录模块加载失败的信息,以便在后续请求相同模块时能够快速响应错误信息,而不是每次都尝试重新加载。 

初始化:当 Vite 开发服务器启动时,moduleGraph 会初始化并开始监听模块的变化。 

模块添加:每当有新的模块被请求或发现时,moduleGraph 会将其添加到图中,并建立与其他相关模块的关系。 

依赖解析:对于每个模块,moduleGraph 会解析其导入语句,确定其直接依赖项,并将这些依赖项也加入到图中。 

动态更新:当某个模块发生变更时,moduleGraph 根据现有的依赖关系图来确定受影响的所有模块,并通知它们进行相应的更新。 

清理不再使用的模块:如果某个模块不再被任何其他模块引用,moduleGraph 会标记该模块为可删除状态,从而释放内存资源。 

源码位置:packages/vite/src/node/server/moduleGraph.ts

感兴趣的小伙伴可以去研读一下,这里不过多阐述。当我们的文件变化收集机制存在了,主角此时就要闪亮登场了。

handleHMRUpdate

handleHMRUpdate作为HMR最核心的方法,我们不得不好好谈谈它。但在谈论它之前,我们要做一个有趣的实验,打开Vite官网,点击左侧HMR API 菜单,映入眼帘的就是:

interface ImportMeta {
  readonly hot?: ViteHotContext
}

interface ViteHotContext {
  readonly data: any

  accept(): void
  accept(cb: (mod: ModuleNamespace | undefined) => void): void
  accept(dep: string, cb: (mod: ModuleNamespace | undefined) => void): void
  accept(
    deps: readonly string[],
    cb: (mods: Array<ModuleNamespace | undefined>) => void,
  ): void

  dispose(cb: (data: any) => void): void
  prune(cb: (data: any) => void): void
  invalidate(message?: string): void

  on<T extends string>(
    event: T,
    cb: (payload: InferCustomEventPayload<T>) => void,
  ): void
  off<T extends string>(
    event: T,
    cb: (payload: InferCustomEventPayload<T>) => void,
  ): void
  send<T extends string>(event: T, data?: InferCustomEventPayload<T>): void
}

 Vite 的 HMR API 设计也是非常有趣,它基于一套完整的 ESM HMR 规范来实现。此时我们来的Vite项目初始化的demo,打开界面

点击中间的按钮,你会发现数字一直在加,这个时候,我们随意修改一下vue文件的代码比如我在check out旁边加一个1,保存后,你会发现你刚刚按钮点击的数字恢复到了0

这个时候我们来到Vite官网,点击左侧菜单HMR API,上面有一些API的介绍,我们滑到hot.accept(cb)这里,拷贝其中一段代码,如下:

做一点简单的修改,加上newModule.render()这个方法,我们把这个段代码直接扔到demo的vue文件中。此时保存后,我们在去按页面上中间的按钮,当数字改变后,你回来在修改你的vue文件,你会惊奇的发现,你的数字没有在重置恢复到0了。

这是为什么呢?这其实就是import.meta.hot 对象上有一个非常关键的方法accept。它就是用来接受模块更新的,引用官网的话说,你一旦“接受”了这个更新,我们的模块就会产生HMR的边界。

Vite 的 HMR 实际上并不替换最初导入的模块:如果一个 HMR 边界模块重新导出来自依赖项的导入,则它应负责更新这些重新导出的模块(这些导出必须使用 let)。此外,从边界模块向上的导入者将不会收到更新。这种简化的 HMR 实现对于大多数开发用例来说已经足够了,同时允许我们跳过生成代理模块的昂贵工作。 --- 官网

通过这个有趣的实验,我们会HMR的印象分又加了一点点。回归正题,我们在来看看handleHMRUpdate方法。用一张图简单的表示一下

源码位置:packages/vite/packages/vite/src/node/server/hmr.ts

关键代码:

通俗一点的说法就是,获取各种前置条件,判断文件变化,记录文件,根据不同的环境文件类型进行更新操作。热更新我们就了解到这。

6.基于ES Build的依赖预构建

为什么需要预构建?

Vite 的预构建(pre-bundling)是一个优化步骤,用于在开发服务器启动时对依赖项进行打包处理。它解决了传统构建工具(如 Webpack)中存在的一个问题:当项目中存在大量的依赖项或某些依赖项使用 CommonJS 模块格式时,初始加载时间可能会变得非常长。通过预构建,Vite 能够显著提高开发服务器的启动速度和热模块替换(HMR)的效率。 

同时,我们也可以参考一些官方的说明

 Vite预编译之后,将文件缓存在node_modules/.vite/文件夹下。根据以下地方来决定是否需要重新执行预构建。 package.json中:dependencies发生变化包管理器的lockfile 如果想强制让Vite重新预构建依赖,可以使用--force启动开发服务器,或者直接删掉node_modules/.vite/文件夹 。

为什么选择esbuild?

esbuild是用 Go 语言编写的,并且针对 CPU 密集型任务进行了优化。它在解析、转换 JavaScript/TypeScript 代码等方面的速度远超基于 JavaScript 实现的传统构建工具(如 Babel 和 Webpack 使用的 acorn 解析器)。这种速度优势使得 Vite 在开发模式下能够实现近乎即时的模块热更新。

这esbuild官方首页里面的图,差距多大,一目了然。

实现原理

打开我们初始化的demo里面node_modules文件夹下,点击.vite文件夹,你会看到一个_metadata.json文件,里面大概是这样子的:

回到Vite官网,依赖预构建菜单的内容里面有一段话,就是它的核心原理所在

Vite 将预构建的依赖项缓存到 node_modules/.vite 中。它会基于以下几个来源来决定是否需要重新运行预构建步骤:

  • 包管理器的锁文件内容,例如 package-lock.jsonyarn.lockpnpm-lock.yaml,或者 bun.lockb
  • 补丁文件夹的修改时间;
  • vite.config.js 中的相关字段;
  • NODE_ENV 的值。

只有在上述其中一项发生更改时,才需要重新运行预构建。

如果出于某些原因你想要强制 Vite 重新构建依赖项,你可以在启动开发服务器时指定 --force 选项,或手动删除 node_modules/.vite 缓存目录。

这个时候我们来看看它的源码是怎么实现的?

源码位置:packages/vite/src/node/optimizer/index.ts

runOptimizeDeps 函数的主要职责是优化项目依赖项,使用 esbuild 工具进行打包和缓存,并提供取消和提交操作的功能。

它通过以下步骤实现这一目标:

初始化:创建临时目录和必要的文件,初始化元数据。 

定义清理和提交逻辑:定义 cleanUp 和 commit 函数,确保在取消或完成优化时正确处理临时文件和元数据。 

处理无依赖情况:如果没有需要优化的依赖项,则直接返回成功结果。 

准备和运行 esbuild 优化:使用 esbuild 打包依赖项,更新元数据,并处理输出文件。 

异常处理:捕获并处理可能出现的异常,确保系统的稳定性。

返回结果:返回包含 cancel 和 result 属性的对象,允许外部代码控制优化过程。

回头看预构建这一步,其实Vite就是做了我们上面那段核心原理的事情,甚至我们可以压缩到一张流程图来表示:

7.总结

Vite工具快无非就体现在以下几点:

1. 原生ES模块支持无打包启动:Vite利用现代浏览器对ES模块(ESM)的原生支持,开发环境无需进行打包。浏览器可以直接解析ES模块,按需加载和执行模块。 

2. 快速冷启动、初始化速度快:Vite的开发服务器启动时,只需要加载配置文件和必要的中间件,然后立即启动服务器。 预构建:Vite在首次启动时会对项目依赖进行预构建,将CommonJS、UMD等格式的模块转换为ESM格式,并缓存到node_modules/.vite目录中。后续启动时,如果依赖未发生变化,可以直接使用缓存,进一步加快启动速度。 

3. 高效的热更新(HMR)精确的模块更新:Vite的HMR机制通过WebSocket与浏览器建立连接,当文件发生变化时,服务器会通知浏览器只重新加载和编译发生变化的模块,而不是整个应用。 

当然工具的发展就是为了提升我们的工作效率,“工欲善其事,必先利其器”。未来可能会有好的工具产生,但当下Vite是非常棒的选择,期待Vite未来更好的发展,给我们带来更优质的开发体验。

8.附录

Vite官网

ESM

esbuild官网

esm-hmr

知乎大佬文章

神三元大佬小册

Vite源码