-
Notifications
You must be signed in to change notification settings - Fork 128
Description
认识javascript modules
Vite 主要是用了 js modules,在认识 vite 之前,我们先简单学习一下 javascript modules。 MDN 的 javascript modules 文档 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#aside_%E2%80%94_.mjs_versus_.js
简单的说,支持javascript modules的浏览器,可以通过script标签引入一个 esm 模块,并且支持其中的import和 export语法,在解析到对应的语法后会在浏览器中动态加载对应的js文件。
我们写一个简单的测试应用来理解什么是 javascript modules,代码都在这里 https://github.com/lihongxun945/javascript-modules-test
入口文件 index.html,通过script标签加载一个模块:
<html>
<body>
<script type="module" src="./index.js"></script>
</body>
</html>index.js引用了另一个模块,并初始化:
import Person from './person.js';
const person = new Person();person.js文件直接导出一个 Person类:
export default function Person () {
console.log('person');
}启动一个静态服务器,直接访问 index.html,可以发现上述代码无需任何编译,在浏览器中可以直接运行。且浏览器会通过网络请求分别加载 index.js 和 person.js
认识vite
为什么需要vite
浏览器已经原生支持了 javascript modules,为什么还要vite呢?有以下几个原因:
- 对非JS文件,比如 css、图片等的支持
- 对非
esm语法的支持 - 通过
bundle优化性能,可以把多个小文件合并成大文件避免浏览器加载成百上千个文件
当然还有一些其他原因,比如生产环境打包、HMR、chunks等
vite 目录结构

vite 默认把index.html放在了项目的根目录,这和我们的webpack项目放在public中有挺大区别。vite 官网上对这个设计做了解释,总结一下主要原因是对于使用 javascript modules的项目来说,index.html 本来就是编译入口文件也是Server的根路径,也就是说既应该放在 src也应该放在 public 中,所以干脆直接放在根目录,这样也不用写 PUBLIC_URL 之类的代码,既符合已有规范还方便,所以就这么写了。
那webpack 为啥不这样做? 因为 webpack 的编译入口文件其实是 index.js而不是index.html,而Server的根路径其实是编译后的 index.html,所以就没这么设计。
加载js和CSS
示例中的 index.html通过如下代码加载 main.tsx:
<script type="module" src="/src/main.tsx"></script>这直接用了 javascript modules 能力
加载的 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')
)TS显然无法被浏览器识别,而在浏览器中加载的文件已经经过了 vite的编译,编译后的代码如下:
var _jsxFileName = "/Users/hongxun.lhx/github/my-vue-app/src/main.tsx";
import __vite__cjsImport0_react from "/node_modules/.vite/react.js?v=8ca9e3e0"; const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react;
import __vite__cjsImport1_reactDom from "/node_modules/.vite/react-dom.js?v=8ca9e3e0"; const ReactDOM = __vite__cjsImport1_reactDom.__esModule ? __vite__cjsImport1_reactDom.default : __vite__cjsImport1_reactDom;
import "/src/index.css";
import App from "/src/App.tsx";
import __vite__cjsImport4_react_jsxDevRuntime from "/node_modules/.vite/react_jsx-dev-runtime.js?v=8ca9e3e0"; const _jsxDEV = __vite__cjsImport4_react_jsxDevRuntime["jsxDEV"];
ReactDOM.render(/* @__PURE__ */ _jsxDEV(React.StrictMode, {
children: /* @__PURE__ */ _jsxDEV(App, {}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 8,
columnNumber: 5
}, this)
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 7,
columnNumber: 3
}, this), document.getElementById("root"));我们来看一看代码是怎么编译的。
main.tsx依赖的 React 和 ReactDOM 经过了 vite 的编译,且缓存在了 node_modules/.vite 目录中。这样做好处是只需要加载两个文件,如果不处理那么浏览器需要加载很多React的依赖。根据官网的说明,vite启动时会自动分析node_modules依赖并把他们都打包,所以并不会因为这些依赖太多导致浏览器加载大量js。
由于打包是在本地进行的,冷启动显然会受到影响,经过自己本地实际测试,冷启动有打包 React相关依赖,热启动无需打包,启动速度分别是:
- 冷启动
435ms - 热启动
236ms
虽然冷启动确实慢了一些,但是不得不说esbuild打包React只多用了100ms,相比webpack依然有大幅提升。
index.css 显然也会被编译成 JS,否则 import 会报错,我们看看编译后的 index.css:
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/index.css");
import { updateStyle, removeStyle } from "/@vite/client"
const id = "/Users/hongxun.lhx/github/my-vue-app/src/index.css"
const css = "body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n"
updateStyle(id, css)
import.meta.hot.accept()
export default css
import.meta.hot.prune(() => removeStyle(id))编译的结果和我们想象的差不多,这里有一个 updateStyle方法,起作用就是通过style标签把CSS插入到文档中,方法的实现如下:
function updateStyle(id, content) {
let style = sheetsMap.get(id);
{
if (style && !(style instanceof HTMLStyleElement)) {
removeStyle(id);
style = undefined;
}
if (!style) {
style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.innerHTML = content;
document.head.appendChild(style);
}
else {
style.innerHTML = content;
}
}
sheetsMap.set(id, style);
}因为我们是 import index.css 的写法,所以export default css实际 并没啥用,如果是用CSS Module写法就有用了。
回到 main.tsx,JSX被编译成了JS是常规操作,对 App.tsx的处理也和上面的逻辑类似。
静态资源
App.tsx中加载了一张图片:
import logo from './logo.svg'根据用Webpack的经验,显然 logo.svg也会被编译成JS,并返回文件地址,实际也确实是这样:
export default "/src/logo.svg"Vite快在哪里?
认识了Vite的基本原理之后,就可以明白vite为什么在本地开发那么快了。主要是基于以下几点:
- 本地只有冷启动的时候有资源打包,热启动完全无任何打包编译操作,只是启动了一个服务器,所以通常 0.2 秒就能完成启动
- 浏览器直接通过
esmodule加载src文件,按需编译单个文件。 node_modules依赖被打包成大文件,避免了浏览器加载多个js。- 公共依赖开启缓存缓存,根本不用请求到 server。
- 最后也是最重要的:
esbuild打包速度无敌快。
那么esbuild 为什么这么快呢?

官方是有解释的,可以看这里:https://esbuild.github.io/faq/#why-is-esbuild-fast
总结一下几个主要原因:
- GO语言的优势:esbuild是用go语言写的,是编译型语言,且针对多线程进行优化,而webpack是用JS写的,解释型且主要是单线程的特性注定他的性能比不过GO。
- 良好的并行优化:多线程优化,尽可能用到全部的CPU核心,且多线程可以共享内存数据。
- 从底层实现:自己实现了底层逻辑,没有依赖三方库。比如TS解析的时候如果用TS官方的编译器,需要检查类型,因此就会变慢;GO的静态类型速度也快于JS中的动态类型。
- 更小的内存使用:更小的内存数据,更少次数对JS进行遍历。
esbuild有这么多优点,那么有没有缺点呢?
当然有的,esbuild的快其实来源于两部分:一部分是 GO语言和多线程带来的优势,另一个部分其实是舍弃了一些特性换来的速度提升。比如 esbuild 省略了语法检查,官方文档中明确说明了esbuild没有做TS类型检查,实际我测试发现JS语法检查也没做;没有生产环境需要用到的代码分割等特性(但有计划做)。因为这些不完善的地方,在打包生产环境代码的时候,vite依然用的是 rollup。