前言: 看到站内很多关于 “vite为什么这么快?” 这个问题,自己也是产生了浓厚的兴趣,于是有了本篇文章的动机。
First of all,这个问题其实是一个伪命题。“快”在这里是一个形容词,我们其实想问的是它为什么开发体验很快,但其实它是需要参照物来进行比较,然后才能得出谁快谁慢的。就好像我们在学校学习的时候,“李雷的英语成绩很好”,实际上李雷的成绩只有30分,说出这句话的人是只考了5分的王梅梅。所以本篇文章会以 webpack 作为对照组,来分析一下为什么使用 vite 开发体验这么丝滑。
一. 准备工作
-
我们为了对比,需要准备用 vite 搭建的工程,和 webpack 搭建的工程(以 vue 为例)。因为 @vue/cli 的 dev-server 内部封装的就是 webpack,故我们直接采用 @vue/cli 来创建 webpack 作为对照组。
-
分别运行以下命令
pnpm create vite vite-test --template vue
npx @vue/cli create webpack-test
-
这是我们目前的目录结构。
二. 排除误区
-
首先我们需要理清楚概念,我们这里说的 vite 比 webpack 快,绝大部分场景是特指在开发环境下,对于开发人员的开发体验来讲,vite 要比 webpack 体验好很多。并不是指代码运行速度快,代码运行效率这个问题并不是构建工具关心的,两者打包后的产物 js 文件,在生产环境下的运行速度,如果两者没有进行十分特殊的配置,其实相差无几。
ps: 打包后不同的文件大小,影响的也只是网络传输速度,和代码真正的运行效率无关。
-
我们分别在各自的目录下运行
pnpm run build
命令,然后从 dist 目录下可以看到各自的打包结果,从打包结构来看是没有什么区别的。你可能会注意到 webpack 的 js 打包结果里有两个文件。其中一个叫 chunk-vendor。这里包含了前端优化的一个点,就是浏览器缓存问题。我们先来讲解一下为什么要这样做?
-
首先我们要搞清楚的是,这个 dist 目录就是我们要上传到服务器的最终代码,当用户通过浏览器访问我们的界面时,会请求
index.html
的内容,当浏览器解析到script
标签时,会再次发起一个请求来获取对应的 js 文件。 -
而我们的前端项目结构往往是这样的,一部分 js 是我们自己写的代码,另一部分是引用的一些第三方库。随着我们的界面日益完善,功能越来越多,我们自己书写的这一部分代码是会很频繁的更新迭代的,而这些第三方库是基本上不变的。如果我们把所有的 js 代码都放在同一个文件里,即使每次改动很小,浏览器也会重新请求所有的 js 代码。
-
很明显这个结果并不是我们期望的,此时我们就会想到,把第三方库的代码和自己的代码分包来进行最终的打包。(vendor 这个单词的含义就是小贩、供货商,而在这个场景下指的就是第三方库)
-
当用分包模式的时候,我们自己代码更新后由于 hash 值变化,浏览器会重新发起新的请求,而 vendor 的第三方库代码,就可以配合服务器来进行 304 协商缓存减少不必要的网络带宽。
-
在 vite.config 中同样可以设置。 import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue';
// https://vite.dev/config/ export default defineConfig({ plugins: [vue()], build: { rollupOptions: { output: { manualChunks: (id) => { if (id.includes('node_modules')) return 'vendor'; }, }, }, }, });
-
对于打包结果,两者可以通过配置来达到基本一致的结果。所以在这里我想表达的意思是再次强调 vite 的快并不是指代码执行速度,需要你请理清打包工具这个概念。
三. webpack 的 npm run dev
- 当我们在 webpack-test 目录下运行
npm run dev
的时候,webpack 内部其实做了很多工作,这里简单概述一下发生了什么:-
- webpack 是运行在 node 环境中的,首先它会启动 fs 模块来读取你入口文件;
-
- 根据你的入口文件(默认为 src/main.js),来解析你所有模块的相互依赖关系;
-
- 而由于 webpack 本身只支持 js 和 json 文件,所以当解析到
import img from 'asset/icon.png'
等这样的非标准模块时,会调用相关 loader 来协助将它转译成为一个 js 文件,然后 webpack 才可以进行后续的抽象语法树分析;(如下图,通过识别一些特殊的关键字来进一步分析出不同模块的依赖关系)
- 而由于 webpack 本身只支持 js 和 json 文件,所以当解析到
-
- 当解析完一个模块后,会记录该模块的内容,最后绘制出完整的依赖关系图。这张关系图你可以近似理解为以相对路径为 key,以模块内容为 value 的一个数组。
- 当解析完一个模块后,会记录该模块的内容,最后绘制出完整的依赖关系图。这张关系图你可以近似理解为以相对路径为 key,以模块内容为 value 的一个数组。
-
- 最后将所有的代码整合,放入到一个自执行函数里,自执行函数里会调用
"./src/main.js"
所对应的函数体。最终形成一个 chunk 文件,这个chunk 文件也就是我们对应的最终产物。
- 最后将所有的代码整合,放入到一个自执行函数里,自执行函数里会调用
-
- 然后 webpac-dev-server 启动,请求 index.html 文件,解析到
script
标签后,运行自执行函数,随后页面产生。
- 然后 webpac-dev-server 启动,请求 index.html 文件,解析到
-
四. vite 的 npm run dev
-
当我们在 vite-test 目录下运行
npm run dev
的时候:-
- 预构建第三方依赖,vite 会扫描
package.json
中所有的运行时依赖,用 esbuild 在node_modules/.vite/deps
为这些第三方库打包成类似于上面vender
一样的临时文件。当开发服务器启动后,遇到浏览器请求相应的资源可以直接使用预构建的结果,并且通过 memory cache 来减少不必要的网络请求。
对应在 network 上的表现如下:
- 预构建第三方依赖,vite 会扫描
-
- vite 会直接启动一个 http-server,它不会像 webpack 那样需要分析整个依赖关系以后再启动;
-
- 此时浏览器会请求
index.html
文件,解析到script
标签以后,发现引用了main.js
,于是浏览器会请求main.js
文件。
- 此时浏览器会请求
-
- 此时我们的界面又引用了 vue 模块一个
app.vue
组件和一些别的模块,对于浏览器来讲,每一个 import 都会创建一个新的请求去获取资源,浏览器需要什么,服务器就按需返回什么,没用到的浏览器也不会产生请求。
- 此时我们的界面又引用了 vue 模块一个
-
- 那么问题来了,浏览器压根就不认识
App.vue
这种文件,那界面又是怎么产生的呢?当我们打开网络这一栏,你会发现,App.vue
对应请求的返回结果是一个 js 文件,并不是浏览器不认识的所谓的.vue
文件,这其实是 vite 开发服务器的按需编译的能力实现的。也就是说 vite 的开发服务器并不是一开始就编译好文件,等待浏览器请求,而是当请求到达时,才进行编译然后返回对应的资源代码。
- 那么问题来了,浏览器压根就不认识
-
五. 开发体验差异总结
-
vite 有使用 esbuild 提前打包依赖的预构建过程,webpack 没有这个优化。
-
vite 无需提前构建依赖关系,直接启动开发服务器,然后利用浏览器特性按需加载、即时编译模块;而 webpack 需要根据入口文件,进行词法分析,语法分析后生成抽象语法树,获取依赖关系,构建出一个完整的 bundle 才可以启动开发服务器。
-
vite 检测某个模块内容改变时,通过改变 contenthash 让浏览器
重新请求
该模块即可,这大大减少了热更新的时间。而 webpack 一个模块或其依赖的模块内容改变时,需要重新编译
解析这些模块。