Vite2.0 正式发布,凭什么这么快 ?

6,095 阅读9分钟

相信随着尤雨溪卖力的吆呼,不少前端的小伙伴已经对 vite 有所耳熟了,这是一个伴随着着 Vue3.0 发布的下一代构建工具,其名字就是出自法语单 fast。可以看到尤雨溪近期的主要工作全部是在 vite 上,足以说明其重要之处 image.png

在这篇文章中,我们就来看看 vite 到底有何独到之处。

天下苦 webpack 久矣

在前端这块混,肯定不能没有听说过 webpack,现代的大型前端应用一般都是用这玩意构建的,但是随着项目的增大,大家发现了 webpack 几个「难言之隐」

  • 随着项目大小增长,项目冷启动时间指数增长
  • 热更新时间也会随着项目大小增大而增长

所以才有了我们常戏称的:npm run dev ,然后去上个厕所,喝杯咖啡,回来可能还没跑完,极大的影响了开发效率。虽然 webpack 提供了很多方法去做构建优化,在日益庞大的前端项目中仍然不太够用,能不能有一种方式一劳永逸的解决这个问题呢?答案就是 Vite !

Vite 横空出世

vite 是如何解决这个世纪难题的呢,答案是  native ES modules。在浏览器没有原生模块化支持的时代,我们往往需要通过 webpack 等构建工具将整个项目打包成一个 js 文件,方便浏览器进行调用。但是随着浏览器厂商的不断努力,现代浏览器基本已经全部支持了 import/ export 语法,下图可以看到具体的浏览器兼容情况。 image.png

大部分浏览器都已经可以解析 js 文件中的 import 语句,webpack 的打包过程是不是有点脱了裤子放屁的感觉了。 ​

如果要一个字概括 vite 的话,那就是快,这是尤大在 vite PPT 中列出来的数据 ​

  • 服务启动时间 < 300ms
  • 模块热替换时间(HMR) < 100ms

从数据上来看确实是吊打 Webpack,我们来看看它是如何做的

为什么 Webpack 这么慢

前文也提到,在之前的浏览器中没有模块化的设计,所以期望把所有源代码编译进一个 js 文件中提供给浏览器使用,所以在开发中当我们运行启动命令的时候,webpack 总是需要从入口文件去索引整个项目的文件,编译成一个或多个单独的 js 文件,即使采用了代码拆分,也需要一次生成所有路由下的编译后文件(这也是为什么代码拆分对开发模式性能没有帮助)。这也导致了服务启动时间随着项目复杂度而指数增长 image.png

Vite 是如何解决问题的

vite 是如何通过使用 native ES modules 优化服务启动时间的呢,使用 Vite 启动开发服务器的时候并不需要提前编译文件(其实是有一个类似过程的,下文详述),而是在浏览器请求对应 URL 的时候,再提供对应的文件,这就实现了在使用了路由懒加载的项目中,仅提供对应路由下的模块的编译文件,而没有索引全部代码的这一过程,项目启动时间始终为常量级。并不会随着项目复杂度变高而一直增长,我们来看看具体是怎么做的 image.png

Vite 是如何实现的

依赖预构建

对 native ES modules 了解的同学可能会想到,它是不支持如下的裸模块导入的,那咋办?

import { someMethod } from 'my-dep'

我们来看看 Vite 是如何实现的,可以直接打开使用 yarn create @vitejs/app 创建的基于 react+ts 的项目这是src/main.tsx 的代码

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

然后把项目跑起来,这是经过Vite 编译后的 src/main.tsx 文件

image.png

可以看到之前的 from 'react' 被重写为了 from "/node_modules/.vite/react.js?v=432aac16"

看起来 Vite 会将预构建的依赖缓存到 node_modules/.vite 路径下,可以看到文件名后跟着一串随机字符串,很容易就可以想到是用来控制浏览器缓存相关的,让我们打开 react.js?v=432aac16 这个文件的请求头看一看:

image.png

果不其然,Cache-Control 属性被写为了: max-age=31536000,immutable ,将这个文件设置为了永久的强制缓存,也就是永远从本地取文件,然后通过向文件名中添加 hash 值控制版本更新。这样一来将依赖文件的缓存判断交给了浏览器老大哥,减少了 Vite 端的工作量,实在是妙啊。

你以为这就完了吗,其实并没有,打开 react.js?v=432aac16 文件一看 ​

image.png

这是什么玩意? 为什么不是熟悉的 react 好兄弟,Vite 还偷偷的干了什么? 没错,在这一步 Vite 并不是只是简单的重写了一下路径。在服务启动的时候,vite 将会在所有源代码中检查类似 import { someMethod } from 'my-dep' 的裸模块导入语句,并执行以下操作

  • 预构建
  • 重写为合法的 URL

其中「预构建」就是上文提到过的「提前编译文件」,在项目启动之初,Vite 会使用 esbuild 进行「依赖预构建」,有两个目的

1.CommonJS 和 UMD 兼容性 在开发阶段中, Vite 的开发服务器将所有的代码都视为原生 ES 模块,所以需要在预构建阶段先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。

2.优化加载性能 Vite将带有许多内部模块的ESM依赖转换为单个模块,以提高后续页面加载性能(降低请求数量),比如 lodash-es 有超过 600 个内置模块,一次性发送 600 多个 http 请求,就算是采用了 HTTP2 也是不可接受的,大量的网络请求在浏览器端会造成网络拥塞,导致页面的加载速度相当慢,通过预构建 lodash-es 成为一个模块,就只需要一个 HTTP 请求了! ​

关于「依赖预构建」更多详细信息可以参考官方文档

预构建完依赖项之后,再使用 es-module-lexer + magic-string 进行轻量级裸模块导入语句的重写。因为并没有进行完整的 AST 遍历,所以速度非常快,对于大多数文件来说这个时间都小于 1ms !

文件编译

解决完裸模块导入的问题,就能愉快的开发了吗,其实并没有。注意到我们的项目是使用的 TypeScript+React 构建的,浏览器是没办法直接解析 tsx 的,老办法,直接看看编译后的文件 image.png 没错看到了熟悉的 React.createElement,熟悉 webpack 的同学应该已经马上想到了,这不是 babel-loader 的工作吗,在 Vite 中是并没有 loader 功能,是如何实现的呢,答案就是 esbuild,Vite 使用 esbuild 作为部分文件类型的解析器(如 TSX & TypeScript),Vite 并不会与 webpack 一样,提前将所有文件编译为浏览器可以接受的类型,而是在接收到浏览器发起的 http 请求之后再去编译对应文件,提供给浏览器。这样会有一个非常合理的疑问,速度够么?

每一次页面加载都需要编译一次文件,看起来会对加载速度有影响啊?这就不得不再次提到 Vite 所使用的构建工具 esbuild,是一个使用 Go 编写的构建工具,按照官网描述,速度比现有的构建工具(webpack 不是在说你)快了10-100倍。其官网有更多详细性能测试数据 image.png

为什么 esbuild 这么快?

为什么同样是构建工具,esbuild 这么优秀?具体有以下几点(来自esbuild 官网

1. 使用 Go 编写,并且编译成了机器码

现在的构建工具一般都是用 JavaScript 进行编写的,对于这种解释型语言(动态语言)来说,在命令行下的性能非常糟糕。因为每次运行编译的时候 V8 引擎都是第一次遇见代码,无法进行任何优化措施。而 esbuild 使用 Go 这种编译型语言(静态语言)编写而成,已经编译成了机器可以直接执行的机器码。当 esbuild 在编译你的 javaScript 代码的时候,可能 Node 还在忙着解析你的构建工具的代码。

除此之外,Go 语言是为并发而设计的语言,而 JavaScript 明显不是(老单线程了)。

  • Go 在线程之间共享使用内存空间,而 JS 想要在线程间传递数据还需要把数据序列化之后再传送。
  • Go 和 JS 的并发都有相应的垃圾回收机制,Go 会在所有线程之间共享堆,对于 JS 而言,每一个线程都有一个独立的堆。

根据 esbuild 的作者的测试,这似乎将 JavaScript 的工作线程的并行处理能力减少了一半,可能是因为你的一半 CPU 核心忙于为另一半收集垃圾

2. 大量使用并行算法

除了 Go 语言天生对于并发的优势,使得其处理并发任务性能远远优于 JavaScript, Esbuild 的内部算法也是经过精心设计的,尽可能的压榨所有的 CPU 核心。

3. esbuild 的所有内容都是从零编写的

自己编写一切而不是使用第三方库有很多性能上的好处。可以从一开始就考虑到性能,可以确保所有的东西都使用一致的数据结构以避免昂贵的转换,当然,缺点就是这工作量非常大。

4. 更有效利用内存

  • esbuild 通过减少 AST 遍历次数(三次),来减小内存访问速度对于打包速度的影响
  • Go 语言还有一个好处就是可以把数据更加紧凑的储存在内存中,从而使得高速 CPU 缓存可以存下更多的内容

Vite2.0 刚刚发布稳定版,其中就把预构建的工具从 Rollup 换成了 esbuild,性能加快了几十倍。

Vite 功能

了解完一些原理,来看看 Vite 到底能干啥?

模块热重载

Vite 支持 Vue 和 React 的模块热重载,可以准确的更新页面而无需重新加载页面或者删除应用程序状态

TypeScript

Vite 支持开箱即用的引入 .ts 文件,但是值得注意的是 Vite 仅执行 ts 文件的翻译工作,并不执行任何类型检查,这是因为 Vite 使用 esbuild 进行转译工作,而并不含类型信息,所以不支持 TypeScript 的特定功能如「常量枚举」「隐式 type-only 导入」。你必须在你的 tsconfig.json 中的 compilerOptions 里设置 "isolatedModules": true,这样 TS 才会警告你哪些功能无法与独立编译模式一同工作。 ​

Vue

作为亲兄弟,Vite 自然会为 Vue 提供第一优先级的支持

JSX

前端另一巨头 React 同样不用担心,.jsx 与 .tsx 也是开箱即用。其翻译是通过 ESBuild 进行的,默认为 React16 形式,React17 形式的 JSX 在 esbuild 中的支持请看这里

CSS

导入 .css 文件会把内容插入到 标签中,同样支持热重载。也能够以字符串的形式检索处理后的、作为其模块默认导出的 CSS。 也支持 「@import 内联」「PostCSS」「CSS Modules」「CSS 预处理器」,详细内容可以看文档

限于篇幅只是简单的列举了几种常用的文件类型的支持,其实 Vite 还支持很多文件类型如「JSON」「Web Assembly」「Web Worker」,可以去官方文档查看更多

写在最后

关于 Vite 其实还有很多想写的,比如 生产环境下的优化,服务端渲染等,但是限于时间与篇幅与能力,只能遗憾的留给下一篇文章了。不会鸽太久的!

参考资料