-
Vite优化了异步代码加载;
-
Vite支持动态引入 polyfill;
-
Vite官方的 legacy mode plugin,可以同时生成 ESM 和 NO ESM;
-
First Class Vue Support。
第5点Vite官网有详细介绍,在非优化方案中,当A
导入异步块时,浏览器必须先请求并解析,A
然后才能确定它也需要公共块C
。这会导致额外的网络往返:
Entry —> A —> C
Vite通过预加载步骤自动重写代码拆分的动态导入调用,以便在A
请求时并行C
获取:
Entry —> (A + C)
可能C
会多次导入,这将导致在未优化的情况下发出多次请求。Vite的优化将跟踪所有import,以完全消除重复请求,示意图如下:
第8点的First Class Vue Support,虽然在列表的最后一项,实则是点睛之笔。
源码分析
Vite在启动时,如果不是中间件模式,内部默认会启一个http server。
exportasyncfunction createServer(
inlineConfig: InlineConfig = {}
): Promise {
// 获取 config
const config = await resolveConfig(inlineConfig, ‘serve’, ‘development’)
const root = config.root
const serverConfig = config.server || {}
// 判断是否是中间件模式
const middlewareMode = !!serverConfig.middlewareMode
const middlewares = connect() as Connect.Server
// 中间件模式不创建 http 服务,允许外部以中间件形式调用:https://Vitejs.dev/guide/api-javascript.html#using-the-Vite-server-as-a-middleware
const httpServer = middlewareMode
-
? null
- await resolveHttpServer(serverConfig, middlewares)
// 创建 websocket 服务
const ws = createWebSocketServer(httpServer, config)
// 创建文件监听器
const { ignored = [], …watchOptions } = serverConfig.watch || {}
const watcher = chokidar.watch(path.resolve(root), {
ignored: [‘/node_modules/’, ‘/.git/’, …ignored],
ignoreInitial: true,
ignorePermissionErrors: true,
…watchOptions
}) as FSWatcher
const plugins = config.plugins
const container = await createPluginContainer(config, watcher)
const moduleGraph = new ModuleGraph(container)
const closeHttpServer = createSeverCloseFn(httpServer)
const server: ViteDevServer = {
// 前面定义的常量,包含:config、中间件、websocket、文件监听器、ESbuild 等
}
// 监听进程关闭
process.once(‘SIGTERM’, async () => {
try {
await server.close()
} finally {
process.exit(0)
}
})
watcher.on(‘change’, async (file) => {
file = normalizePath(file)
// 文件更改时使模块图缓存无效
moduleGraph.onFileChange(file)
if (serverConfig.hmr !== false) {
try {
// 大致逻辑是修改 env 文件时直接重启 server,根据 moduleGraph 精准刷新,必要时全部刷新
await handleHMRUpdate(file, server)
} catch (err) {
ws.send({
type: ‘error’,
err: prepareError(err)
})
}
}
})
// 监听文件创建
watcher.on(‘add’, (file) => {
handleFileAddUnlink(normalizePath(file), server)
})
// 监听文件删除
watcher.on(‘unlink’, (file) => {
handleFileAddUnlink(normalizePath(file), server, true)
})
// 挂载插件的服务配置钩子
const postHooks: ((() => void) | void)[] = []
for (const plugin of plugins) {
if (plugin.configureServer) {
postHooks.push(await plugin.configureServer(server))
}
}
// 加载多个中间件,包含 cors、proxy、open-in-editor、静态文件服务等
// 运行post钩子,在html中间件之前应用的,这样外部中间件就可以提供自定义内容取代 index.html
postHooks.forEach((fn) => fn && fn())
if (!middlewareMode) {
// 转换 html
middlewares.use(indexHtmlMiddleware(server, plugins))
// 处理 404
middlewares.use((_, res) => {
res.statusCode = 404
res.end()
})
}
// errorHandler 中间件
middlewares.use(errorMiddleware(server, middlewareMode))
// 执行优化逻辑
const runOptimize = async () => {
if (config.optimizeCacheDir) {
// 将使用 ESbuild 将依赖打包并写入 node_modules/.Vite/xxx
await optimizeDeps(config)
// 更新 metadata 文件
const dataPath = path.resolve(config.optimizeCacheDir, ‘metadata.json’)
if (fs.existsSync(dataPath)) {
server._optimizeDepsMetadata = JSON.parse(
fs.readFileSync(dataPath, ‘utf-8’)
)
}
}
}
if (!middlewareMode && httpServer) {
// 在服务器启动前覆盖listen方法并运行优化器
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number, …args: any[]) => {
await container.buildStart({})
await runOptimize()
return listen(port, …args)
}) as any
httpServer.once(‘listening’, () => {
// 更新实际端口,因为这可能与初始端口不同
serverConfig.port = (httpServer.address() as AddressInfo).port
})
} else {
await runOptimize()
}
// 最后返回服务
return server
}
访问Vite服务的时候,默认会返回index.html:
处理 import 文件逻辑,在 node/plugins/importAnalysis.ts 文件内:
exportfunction importAnalysisPlugin(config: ResolvedConfig): Plugin {
const clientPublicPath = path.posix.join(config.base, CLIENT_PUBLIC_PATH)
let server: ViteDevServer
return {
name: ‘Vite:import-analysis’,
configureServer(_server) {
server = _server
},
async transform(source, importer, ssr) {
const rewriteStart = Date.now()
// 使用 es-module-lexer 进行语法解析
await init
let imports: ImportSpecifier[] = []
try {
imports = parseImports(source)[0]
} catch (e) {
const isVue = importer.endsWith(‘.vue’)
const maybeJSX = !isVue && isJSRequest(importer)
// 判断文件后缀给不同的提示信息
const msg = isVue
-
?
- maybeJSX ?
- `You may need to install appropriate plugins to handle the ${path.extname(
Install @Vitejs/plugin-vue to handle .vue files.
If you are using JSX, make sure to name the file with the .jsx or .tsx extension.
importer
)} file format.`
this.error(
Failed to parse source for import analysis because the content
+
contains invalid JS syntax.
+
msg,
e.idx
)
}
// 将代码字符串取出
let s: MagicString | undefined
const str = () => s || (s = new MagicString(source))
// 解析 env、glob 等并处理
// 转换 cjs 成 esm
}
}
}
拿Vue的NPM包举例经优化器处理后的路径如下:
// before
- import { createApp } from’vue’
- import { createApp } from’/node_modules/.Vite/vue.runtime.esm-bundler.js?v=d17c1aa4’
import App from’/src/App.vue’
createApp(App).mount(‘#app’)
截图中的/src/App.vue路径经过Vite处理发生了什么?
首先需要引用 @Vitejs/plugin-vue 来处理,内部使用Vue官方的编译器@vue/compiler-sfc,plugin处理逻辑同rollup的plugin,Vite在Rollup的插件机制上进行了扩展。
详细可参考:https://Vitejs.dev/guide/api-plugin.html,这里不做展开。
编译后的App.vue文件如下:
import { createHotContext as __Vite__createHotContext } from"/@Vite/client";
import.meta.hot = __Vite__createHotContext(“/src/App.vue”);
import HelloWorld from’/src/components/HelloWorld.vue’
const _sfc_main = {
expose: [],
setup(__props) {
return { HelloWorld }
}
}
import {
createVNode as _createVNode,
Fragment as _Fragment,
openBlock as _openBlock,
createBlock as _createBlock
} from"/node_modules/.Vite/vue.runtime.esm-bundler.js?v=d17c1aa4"
const _hoisted_1 = /#PURE/_createVNode(“img”, {
alt: “Vue logo”,
src: “/src/assets/logo.png”
}, null, -1/* HOISTED */)
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_hoisted_1,
_createVNode($setup[“HelloWorld”], { msg: “Hello Vue 3 + Vite” })
], 64/* STABLE_FRAGMENT */))
}
import"/src/App.vue?vue&type=style&index=0&lang.css"
_sfc_main.render = _sfc_render
_sfc_main.__file = “/Users/orange/build/Vite-vue3/src/App.vue”
exportdefault _sfc_main
_sfc_main.__hmrId = “7ba5bd90”
typeof VUE_HMR_RUNTIME !== ‘undefined’ && VUE_HMR_RUNTIME.createRecord(_sfc_main.__hmrId, _sfc_main)
import.meta.hot.accept(({ default: updated, _rerender_only }) => {
if (_rerender_only) {
VUE_HMR_RUNTIME.rerender(updated.__hmrId, updated.render)
} else {
VUE_HMR_RUNTIME.reload(updated.__hmrId, updated)
}
})
可以发现,Vite本身并不会递归编译,这个过程交给了浏览器,当浏览器运行到import HelloWorld from ‘/src/components/HelloWorld.vue’ 时,又会发起新的请求,通过中间件来编译 vue,以此类推,为了证明我们的结论,可以看到 HelloWorld.vue 的请求信息:
经过分析源码后,能断定的是,Snowpack与Vite在启动服务的时间会远超Webpack,但复杂工程的首次编译到完全可运行的时间需要进一步测试,不同场景下可能产生截然不同的结果。
功能对比
|
| Vite@2.0.3 | Webpack@5.24.2 | Snowpack@3.0.13 |
| — | — | — | — |
| 支持Vue2 | 非官方支持: https://github.com/underfin/vite-plugin-vue2 | 支持:vue-loader@^15.0.0 | 非官方支持:https://www.npmjs.com/package/@lepzulnag/Snowpack-plugin-vue-2 |
| 支持Vue3 | 支持 | 支持:vue-loader@^16.0.0(https://github.com/Jamie-Yang/vue3-boilerplate) | 支持:https://www.npmjs.com/package/@Snowpack/plugin-vue |
| 支持Typescript | 支持:ESbuild (默认无类型检查) | 支持:ts-loader | 支持:https://github.com/Snowpackjs/Snowpack/tree/main/create-Snowpack-app/app-template-vue-typescript |
| 支持CSS预处理器 | 支持:https://vitejs.dev/guide/features.html#css-pre-processors | 支持:https://vue-loader.vuejs.org/guide/pre-processors.html | 部分支持:官方仅提供了Sass和Postcss,且存在未解决BUG |
| 支持CSS Modules | 支持:https://vitejs.dev/guide/features.html#css-modules | 支持:https://vue-loader.vuejs.org/guide/css-modules.html | 支持 |
| 支持静态文件 | 支持 | 支持 | 支持 |
| 开发环境 | no-bundle native ESM(CJS → ESM) | bundle(CJS/UMD/ESM) | no-bundle native ESM(CJS → ESM) |
| HMR | 支持 | 支持 | 支持 |
| 生产环境 | Rollup | Webpack | Webpack, Rollup, or even ESbuild |
| Node API 调用能力 | 支持 | 支持 | 支持 |
启动时编译速度对比
下面一组测试的代码完全相同,都是 Hello World 工程,没有任何复杂逻辑,Webpack 与 Snowpack 分别引入了对应的 Vue plugin,Vite 无需任何插件。
Webpack5 + vue3(1.62s)
工程目录:
控制台输出:
Snowpack3 + vue3(2.51s)
工程目录:
控制台输出:
Vite2 + vue3(0.99s)
工程目录:
控制台输出:
真实项目迁移
测试案例:已存在的复杂逻辑vue工程
经过简单的测试及调研结果,首先从生态和性能上排除了Snowpack,下面将测试Webpack5与Vite2。
迁移Vite2遇到的问题:
1.不支持省略.vue后缀,因为此路由机制与编译处理强关联;
2.不支持.vue后缀文件内写jsx,若写jsx,需要改文件后缀为.jsx;
3.不建议import { … } from "dayjs"与import duration from 'dayjs/plugin/duration’同时使用,从源码会发现在optimizeDeps阶段已经把ESM编译到了缓存文件夹,若同时使用会报错:
4.当optimizeDeps忽略后文件路径错误,node_modules/dayjs/dayjs.main.js?version=xxxxx,此处不应该在query中添加version;
5.组件库中window.$方法找不到,不能强依赖关联顺序,跟请求返回顺序有关;
6.当dependencies首次未被写入缓存时,补充写入会报错,需要二次重启;
7.在依赖关系复杂场景,Vue被多次cache,会出现ESM二次封装的情况,也就是ESM里面嵌套ESM的情况;
种种原因,调试到这里终结了,结论就是Vite2目前处于初期,尚不稳定,处理深层次依赖就目前的moduleGraph机制还有所欠缺,有待完善。
Webpack5
效果和我们之前测试相同代码在Webpack4下50+秒相比提升明显,实际场景可能存在误差,但WebpackConfig配置细节基本一致。
编译压缩提速
不知大家是否有遇到这个问题:
<— Last few GCs —>
[59757:0x103000000] 32063 ms: Mark-sweep 1393.5 (1477.7) -> 1393.5 (1477.7) MB, 109.0 / 0.0 ms allocation failure GC in old space requested
<— JS stacktrace —>
==== JS stack trace =========================================
Security context: 0x24d9482a5ec1
…
或者在 92% 的进度里卡很久:
Webpack chunk asset optimization (92%) TerserPlugin
随着产物越来越大,编译上线和CI的时间都越来越长,而其中1/3及更多的时间则是在做压缩的部分。OOM的问题也通常来源于压缩。
如何解决压缩慢和占内存的问题,一直是逃避不开的话题,Vite采用了ESbuild,接下来分析一下ESbuild。
ESbuild
下面是官方的构建时间对比图,并没有说明场景,文件大小等,所以不具备实际参考价值。
之所以快,其中最主要的应该是用go写,然后编译为Native代码。然后npm安装时动态去下对应平台的二进制包,支持Mac、Linux和Windows,比如esbuild-darwin-64。
相同思路的还有es-module-lexer、swc等,都是用编译成Native代码的方式进行提速,用来弥补Node在密集CPU计算场景的短板。
ESbuild有两个功能,bundler和minifier。bundler的功能和babel以及Webpack相比差异很大,直接使用对现有业务的风险较大;而minifier可以尝试,在Webpack和babel产物的基础上做一次生产环境压缩,可以节省terser plugin的压缩时间。
同时针对Webpack提供了 esbuild-webpack-plugin,可以在 Webpack 内直接使用 ESbuild。
优缺点及总结
Snowpack
缺点:
-
社区不够完善,无法支撑我们后续的业务演进;
-
编译速度提效不明显。
Vite
优点:
-
因其与rollup联合,社区里rolllup的插件基本都可以直接使用,社区相对完善;
-
编译速度快。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后
分享一套阿里大牛整理的前端资料给大家,点击前端校招面试题精编解析大全即可免费下载
❤️ 谢谢支持,喜欢的话别忘了 关注、点赞哦。
ESbuild。
优缺点及总结
Snowpack
缺点:
-
社区不够完善,无法支撑我们后续的业务演进;
-
编译速度提效不明显。
Vite
优点:
-
因其与rollup联合,社区里rolllup的插件基本都可以直接使用,社区相对完善;
-
编译速度快。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-7flamXRj-1713716660154)]
[外链图片转存中…(img-f6pRs9Zo-1713716660154)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-WGOfmj5O-1713716660154)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
[外链图片转存中…(img-jfpqs6ct-1713716660155)]
最后
分享一套阿里大牛整理的前端资料给大家,点击前端校招面试题精编解析大全即可免费下载
❤️ 谢谢支持,喜欢的话别忘了 关注、点赞哦。