“浏览器如何处理 ESM 模块” 其实浏览器处理 Vite 提供的 ESM 模块核心就是遵循原生 ESM 规范“按需请求、逐个加载”整个过程和 Vite 的“不打包”逻辑完美配合具体流程拆解如下下面用一张流程图直观展示浏览器从加载入口 HTML 到最终执行渲染的完整链路flowchart TD A[浏览器加载 index.html] -- B[识别 script typemodule] B -- C[解析 main.jsx 的 import 语句] C -- D[递归发起 HTTP 请求依赖] D -- E{Vite 按需编译} E -- F[React 组件: esbuild 编译 JSX/TSX] E -- G[第三方依赖: 预构建 CJS 转 ESM] E -- H[普通 JS/CSS: 直接返回] F -- I[浏览器递归加载所有依赖] G -- I H -- I I -- J[从入口模块执行并渲染] J -- K[WebSocket 热更新通知] K -- L[浏览器重新请求更新模块] L -- M[局部渲染, 不刷新页面]这张图把「浏览器原生行为」和「Vite 的翻译工作」分成了上下两层上半部分是浏览器按 ESM 规范做的加载动作下半部分是 Vite 针对不同模块类型做的编译处理最后通过 WebSocket 实现精准的热更新。1. 第一步浏览器加载入口 HTML触发 ESM 启动Vite 开发服务器启动后你访问 localhost:5173 时浏览器首先加载的是项目根目录的 index.html。这个 HTML 里会有一个关键的脚本标签!-- Vite 自动注入的入口脚本typemodule 是核心 --script typemodule src/src/main.jsx/script• typemodule 是浏览器原生支持 ESM 的标识告诉浏览器“这个脚本是一个 ESM 模块需要按模块化规则加载”。2. 第二步浏览器解析入口模块递归请求依赖当浏览器解析 main.jsxReact 入口文件时会遇到 import 语句比如导入 React 组件、工具函数等例如// main.jsx 中的导入import React from react;import ReactDOM from react-dom/client;import App from ./App.jsx; // 自定义 React 组件此时浏览器会做两件事1. 识别依赖通过 import 语句明确当前模块依赖了 react、react-dom、./App.jsx 三个模块2. 发起请求针对每个依赖自动向 Vite 开发服务器发起 HTTP 请求获取对应的 ESM 模块文件比如◦ 请求 http://localhost:5173/node_modules/react/index.js第三方依赖◦ 请求 http://localhost:5173/src/App.jsx自定义组件3. 第三步Vite 按需编译模块返回给浏览器浏览器的请求到达 Vite 服务器后Vite 会根据“请求的模块类型”做针对性处理• 如果是 React 组件.jsx/.tsxVite 会用 vitejs/plugin-react esbuild 快速编译——把 JSX 语法如 App /转译为 React.createElement(App)同时处理 TS 语法如果是 TSX然后把“编译后的 JS 代码”以 ESM 格式返回给浏览器• 如果是第三方依赖如 reactVite 会先对依赖做“预构建”启动时一次性完成把 CommonJS 格式的依赖很多 npm 包还是 CommonJS转译为 ESM 格式再返回给浏览器• 如果是普通 JS/CSS直接返回原生内容或简单处理后返回。4. 第四步浏览器递归加载所有依赖最终执行浏览器拿到 Vite 返回的“编译后 ESM 模块”后会重复第二步解析这个模块里的 import 语句继续请求它的依赖比如 App.jsx 可能导入了 ./Button.jsx浏览器就再请求 Button.jsx。直到所有依赖模块都加载完成后浏览器会从入口模块main.jsx开始执行代码最终渲染 React 应用// main.jsx 执行逻辑const root ReactDOM.createRoot(document.getElementById(root));root.render(App /); // 执行渲染5. 当模块更新时热更新浏览器如何处理这一步就对应 Vite 热更新的核心1. 你修改了某个组件如 Button.jsxVite 会重新编译这个单个模块然后通过 WebSocket 告诉浏览器“/src/Button.jsx 模块更新了”2. 浏览器收到通知后只需要重新请求“更新后的 Button.jsx 模块”无需请求其他无关模块3. 浏览器用新模块替换旧模块React 会感知到组件变化只重新渲染 Button 组件及其父组件局部渲染不刷新页面。简单总结浏览器处理逻辑的核心浏览器其实只做了它“原生就会做的事”——识别 typemodule 脚本按 import 语句递归请求模块并执行。而 Vite 的作用是“在中间做了层高效的‘翻译官’”把 React 特有的语法JSX/TSX、非 ESM 依赖实时转译为浏览器能懂的 ESM 模块同时精准控制更新范围让整个过程又快又轻量。1. CommonJS 与 ESM 的核心区别在深入 Vite 的模块处理机制之前先厘清 JavaScript 模块体系中最常被问到的两个概念CommonJSCJS和 ES ModuleESM。它们代表了两种不同的模块规范理解它们的差异是理解 Vite 预构建、按需编译等行为的前提。CommonJSCJS由 Node.js 社区提出并广泛使用采用require()加载模块、module.exports导出模块。它是同步加载模块在运行时被解析适合服务端场景。ES ModuleESM是 ECMAScript 官方标准采用import导入、export导出。它是静态分析、异步加载浏览器原生支持适合前端场景。两者核心差异对比如下对比维度CommonJSCJSES ModuleESM加载方式同步加载运行时解析异步加载静态分析导入语法require(module)import xxx from module导出语法module.exports xxxexport default xxx/export { xxx }执行时机运行时执行动态编译时静态解析可静态分析依赖浏览器支持不支持需打包器转换原生支持typemodule循环依赖处理容易出问题部分导出可能为 undefined设计上更健壮支持 live bindingTree Shaking不支持难以静态分析天然支持可静态分析未使用导出这也是为什么 Vite 在开发模式下需要做「预构建」很多 npm 包仍然以 CommonJS 格式发布而浏览器只认 ESM所以 Vite 必须先把这些 CJS 依赖转译为 ESM 格式浏览器才能直接加载。2.问题 React.createElement不是react 得用法吗因为经过 Vite esbuild 的处理后最终给到浏览器的已经不是“带 JSX 的 React 源码”而是纯原生 JavaScript 代码——浏览器识别的是这份“转换后的 JS 代码”而非原始 React 代码核心逻辑分两步1. React 代码被“转译为原生 JS 语法”浏览器不认识的是 React 特有的 JSX 语法如 div /但经过 esbuild 转译后• JSX 会变成 React.createElement(...) 这种原生 JS 函数调用函数调用是浏览器完全能识别的 JS 基础语法• 若有 ES6 语法箭头函数、let/const 等会被降级为 ES5 语法如 var、普通函数适配所有浏览器。例如原始 React 代码// 浏览器不认识的 React 代码含 JSXconst Button () buttonClick/button;转译后给到浏览器的代码// 浏览器能直接识别的原生 JSimport { createElement } from react; // 依赖引用也是原生 ESM 语法const Button () createElement(button, null, Click);2. React.createElement 有“具体的 JS 实现”React.createElement 看似是 React 专属函数但它本质是 react 库导出的一个普通 JS 函数——Vite 会把 react 库本身也处理成浏览器能识别的 ESM 模块其中就包含这个函数的实现。比如浏览器请求 react 模块时Vite 返回的代码里会有类似这样的逻辑简化版// react 模块的核心实现简化export function createElement(type, props, ...children) {// 本质是返回一个“描述 DOM 结构的对象”React 元素return {$$typeof: Symbol.for(react.element),type,props: { ...props, children },// 其他内部属性...};}当浏览器执行 createElement(button) 时其实就是在调用这个“普通 JS 函数”生成一个描述按钮的“React 元素对象”——而这个对象最终会被 react-dom 库同样是 JS 实现转换成真实的 DOM 节点渲染到页面上。核心总结浏览器能识别的关键是 Vite esbuild 做了“翻译和搭桥”• 把“浏览器不认识的 React 语法JSX”翻译成“浏览器认识的 JS 函数调用”• 把“React 库的函数如 createElement”提供成“浏览器能加载的 JS 模块”。最终浏览器执行的全是原生 JS 代码自然就能识别并运行 React 应用了。