Why Vite is Fast?

2022.10.12

我在开发一些项目的时候,我发现 webpack 的 dev server 启动很慢,而 vite 很快就启动起来了,今天我们就来看下 vite 为什么启动那么快。

传统的开发模式都是使用 webpack 一类的 bundler,将所有的文件从入口文件开始,打包(bundle)成一个文件,然后启动一个页面引用这个文件,这样的话,如果项目越大,打包所花的时间就会越多。

https://vitejs.dev/guide/why.html
https://vitejs.dev/guide/why.html

而 vite 这种方式,也就是所谓的 unbundled development,它没有打包这个流程,只会对入口文件进行转译(transpile)将其转换成 ESM 模块,浏览器直接就可以用了,暂时不需要的文件就不用处理了,按需加载。

https://vitejs.dev/guide/why.html
https://vitejs.dev/guide/why.html

那么浏览器在看到 import ... from ... 的时候会做什么?

自己对 import 的语法只停留在了 amd/cjs 等的转译后的模块,在代码库里引入一个三方模块的时候会从 node_modules 里被引入,比如,import React from 'react',或者引入本地模块的时候通过,import App from './App',那么 vite 这种不经过打包直接使用浏览器的原生 ESM 的话,会怎么样呢?

来看下 MDN 上的定义,

module-name

    The module to import from. The evaluation of the specifier is host-specified. This is often a relative or absolute URL to the .js file containing the module. In Node, extension-less imports often refer to packages in node_modules. Certain bundlers may permit importing files without extensions; check your environment. Only single quoted and double quoted Strings are allowed.

比如我们用 vite 脚手架起一个项目 npm create vite@latest my-react-app -- --template react,当我们使用 npm run dev 的时候浏览器会打开我们的本地开发页面,源码如下,

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

然后浏览器加载我们的入口文件 main.jsx,浏览器会向我们的 vite 服务器发送一条请求 http://localhost:5173/src/main.jsx?t=1665496658489,而 main.jsx 的源代码长下面这样,

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

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

浏览器在解析 import App from './App' 这一个 statement 的时候,会往 vite 服务器发送一条请求http://localhost:5173/src/App.jsx 来加载我们的 App 文件,至于后缀省略了 vite 服务器也能识别,我猜应该是做了什么处理,比如默认加载 .js 后缀的文件,其次 .jsx 等的逻辑。

细心的小伙伴肯定也注意到了 import React from 'react' 这个 statement,如果浏览器遇到这个会怎么样?本地开发我们知道这个模块会去 node_modules 里面读取,但是浏览器会怎么做呢?

我将 main.jsx 改成 main.js,然后用 http-server . 代替 vite 来打开本地的 html 页面,html 页面源码如下,

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

然后在 main.js 文件里只写一行 import React from 'react',然后浏览器就会错了,TypeError: Module specifier, 'react' does not start with "/", "./", or "../",就是说浏览器识别不了了。

vite 的做法就是 pre-bundle,将这种全局模块转换成 ESM 模块,然后放到 node_modules/.vite/deps 目录里,然后将 import React from 'react' 这种全局的模块引用转换成 import __vite__cjsImport0_react from "/node_modules/.vite/deps/react.js?v=b6122a24"; 这种浏览器能识别的,这样浏览器在遇到这个 statement 的时候就会发一条请求去 node_modules/.vite/deps 目录里读取 react 模块了。

我们拿最初的例举例,浏览器在加载我们的入口文件 main.jsx 的时候,vite 服务器会将里面的全局模块全替换成 pre-bundled 模块,这样浏览器就可以继续发送请求到 vite 服务器加载其他这些模块了。

这样我们打开页面的时候,我们向 vite 请求 main.jsx 文件的时候,接口返回的文件内容就长这样了,

var _jsxFileName = "/Users/cary/workspace/github/my-react-app/src/main.jsx";
import __vite__cjsImport0_react from "/node_modules/.vite/deps/react.js?v=b6122a24";
const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react;
import __vite__cjsImport1_reactDom_client from "/node_modules/.vite/deps/react-dom_client.js?v=8f1bf833";
const ReactDOM = __vite__cjsImport1_reactDom_client.__esModule ? __vite__cjsImport1_reactDom_client.default : __vite__cjsImport1_reactDom_client;
import App from "/src/App.jsx";
import "/src/index.css";
import { jsxDEV as _jsxDEV } from "/@id/__x00__react/jsx-dev-runtime";
ReactDOM.createRoot(document.getElementById("root")).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));

你会发现这些全局模块就都被转换成浏览器能识别的格式了,比如,import __vite__cjsImport0_react from "/node_modules/.vite/deps/react.js?v=b6122a24";

高级。

参考

  1. https://vitejs.dev/guide/dep-pre-bundling.html
  2. https://vitejs.dev/guide/features.html
  3. how-snowpack-works
  4. how-snowpack-works-v2
  5. A Dev Server Supports ESM
  6. import MDN