JavaScript 在浏览器、Node.js 与 Bun 中的运行差异
JavaScript 的语言语法由 ECMAScript 定义,但 fetch、文件系统、模块解析和事件循环并不属于 ECMAScript。它们由宿主运行时提供。因此,同一段 JavaScript 是否可运行、如何加载依赖、何时执行回调,取决于它运行在浏览器、Node.js 还是 Bun。
先区分语言与运行时
ECMAScript 规定了 class、Promise、模块语法、Map 和 Array 等语言能力。浏览器、Node.js 与 Bun 都实现了这套标准,日常业务代码的语法差异很小。
运行时在语言之外补齐执行引擎、全局对象、I/O、模块加载器、事件循环和安全模型。document 是浏览器 API,fs 是 Node.js API,Bun.file() 是 Bun API;它们都不是 JavaScript 标准的一部分。
| 维度 | 浏览器 | Node.js | Bun |
|---|---|---|---|
| 主要目标 | 渲染和交互网页 | 服务端、脚本和工具链 | 服务端与高性能工具链 |
| JavaScript 引擎 | V8、JavaScriptCore 等 | V8 | JavaScriptCore |
| I/O 边界 | Web API 与用户授权 | 操作系统能力 | 操作系统能力 |
| 模块入口 | URL 和 import | 包解析、ESM、CommonJS | 包解析、ESM、CommonJS 兼容 |
| DOM | 原生提供 | 默认不提供 | 默认不提供 |
| 内置服务能力 | 无监听端口 API | node:http、node:net 等 | Bun.serve()、Node 兼容 API |
不能根据文件扩展名判断运行时。.js 既可以由浏览器加载,也可以由 node 或 bun 执行;决定行为的是启动环境和构建配置。
全局对象与平台 API
浏览器:围绕页面与用户
浏览器的全局环境通常是 window,同时以 globalThis 提供统一入口。核心能力是 DOM、CSSOM、事件、存储、网络请求、计时器和渲染生命周期。
1 | |
页面脚本不能任意读取本机路径、监听 TCP 端口或启动子进程。浏览器以同源策略、CORS、内容安全策略和权限提示限制能力。fetch('https://api.example.com') 能否读取响应,还受目标服务器的 CORS 响应头约束。
浏览器提供的 File、Blob 和 FileSystemHandle 面向用户选择或授权后的文件,而不是 Node.js 风格的任意路径文件系统。不要把它们当作 fs 的替代品。
Node.js:围绕进程与操作系统
Node.js 使用 V8 执行 JavaScript,通过 C++ 绑定和 libuv 提供文件、网络、子进程、DNS、信号和线程池等能力。顶层全局对象是 global,跨运行时代码应优先使用 globalThis。
1 | |
现代 Node.js 已实现许多 Web API,例如 fetch、Request、Response、Web Streams、URL 和 Web Crypto。但它们不意味着 Node.js 具有浏览器环境:没有 DOM、布局、window、localStorage 或页面生命周期。
Node.js 进程默认拥有启动用户可访问的操作系统权限。服务端程序必须自行约束路径、网络目标、子进程参数和不可信输入;浏览器的同源策略不会替你保护服务端。
Bun:以 Web API 为表面,兼容 Node.js 生态
Bun 使用 JavaScriptCore,而不是 V8。它直接提供大量 Web API,并实现 Node.js 的 process、Buffer 和多数 node: 内置模块,以便复用 npm 包。兼容并不等价于完全相同:原生扩展、边缘 API、诊断输出和性能特征都应在 Bun 中实际验证。
1 | |
Bun 还将文件、子进程、SQLite、测试、打包和包管理器纳入运行时接口,例如 Bun.write()、Bun.spawn() 和 bun:sqlite。这些 API 适合明确选择 Bun 的应用,不应放入宣称兼容浏览器或 Node.js 的共享库。
模块加载的差异
浏览器按 URL 加载 ESM
浏览器原生支持 ES Modules。模块说明符必须能解析为 URL,通常是相对路径、绝对路径、完整 URL 或由 import map 映射的裸说明符。
1 | |
1 | |
原生浏览器不会遍历 node_modules,也不能直接理解 TypeScript、JSX 或大多数 npm 包的源代码。Vite、Webpack、Rspack 等构建工具负责解析包、转换语法、打包或开发时按需转换。
Node.js 同时支持 ESM 和 CommonJS
Node.js 支持 ESM 的 import,也保留 CommonJS 的 require()。一个文件采用哪种模式受扩展名和最近的 package.json 中 type 字段影响:.mjs 固定为 ESM,.cjs 固定为 CommonJS,.js 则由 type 决定。
1 | |
Node.js 会按 node_modules、package.json 的 exports 和条件导出解析包。ESM 中应显式写出相对文件扩展名;不要期待浏览器和 Node.js 对 ./util 做同样的猜测。
Bun 原生加载更多文件类型
Bun 能直接执行 TypeScript、JSX、JSON 等常见项目文件,并支持 ESM 与 CommonJS。它同样解析 npm 包和 package.json 导出,因此常可直接运行 Node.js 项目。
这减少了开发期转译配置,不会消除发布边界。若库的消费者包含浏览器、Node.js 和 Bun,应发布已构建的 JavaScript,并用条件导出给不同环境提供明确入口。
1 | |
条件的键顺序有语义:解析器会选择第一个满足的条件。将通用 default 放在末尾,避免更具体的运行时入口被提前遮蔽。
事件循环与异步 I/O
三者都有 Promise、微任务和计时器,但调度器的实现与优先级不完全相同。依赖某个回调的精确先后顺序,尤其混用 setTimeout()、I/O 回调、queueMicrotask() 和运行时私有 API 时,容易产生跨运行时错误。
浏览器的事件循环还要在任务之间协调渲染。长时间同步计算会阻塞点击、动画和绘制;应拆分任务、使用 Web Worker,或将计算移到服务端。
Node.js 的事件循环处理 I/O 阶段,部分文件系统、DNS 和加密任务会使用 libuv 线程池。process.nextTick() 是 Node.js 特有的高优先级队列;递归调度它可能饿死 I/O,因此跨平台库优先用 queueMicrotask()。
1 | |
Bun 的事件循环与 I/O 实现不同于 Node.js。把 Node.js 的时序细节、内存占用或吞吐量结论直接迁移到 Bun 没有依据;性能敏感路径必须用目标运行时、目标操作系统和真实负载测量。
网络、流与二进制数据
浏览器的网络编程以 fetch、WebSocket、EventSource 和 WebRTC 为主,通常以 ReadableStream、Blob、ArrayBuffer 和 Uint8Array 传递数据。请求受同源策略、CORS 和混合内容策略约束。
Node.js 同时提供 Web 风格 API 和传统的 node:stream、node:http、node:net API。两套流模型可以互转,但接口、背压处理和错误传播不同;在一个数据管道中选定一种模型,避免无意义的适配与复制。
Bun 也重视 Web 标准的 Request、Response 与 ReadableStream,并兼容许多 Node.js 网络接口。对外暴露 HTTP 服务时,优先以标准 Request 和 Response 设计边界;仅在需要 Node.js 中间件兼容性时引入 Node 专用接口。
编写可移植代码
共享代码应只依赖 ECMAScript 和三个运行时都稳定实现的 Web 标准,例如 URL、URLSearchParams、TextEncoder、crypto.subtle 与 fetch。即使 API 名称相同,也要确认目标版本和语义满足需求。
平台能力放在应用边缘,再通过小接口注入共享逻辑。下面的函数不关心数据来自浏览器缓存、Node.js 文件还是 Bun 数据库。
1 | |
浏览器入口可传入 () => localStorage.getItem('profile') ?? '{}';Node.js 或 Bun 入口则传入读取文件或数据库的函数。这样,平台差异被限制在入口层,业务逻辑不需要检测运行时。
若确实需要分支,使用能力检测而不是用户代理或版本字符串。能力检测表达真正前提,也能兼容 polyfill 和未来运行时。
1 | |
不要在同一模块顶层同时导入浏览器和 Node.js 专用模块。打包器或浏览器可能在执行分支前就解析失败。应将平台实现拆成独立入口,再用条件导出、构建别名或依赖注入选择它们。
选择运行时
前端页面选择浏览器;需要 DOM 的测试也应使用真实浏览器或专门的 DOM 模拟环境。传统服务端、CLI、需要成熟原生扩展或依赖 Node.js 诊断生态的项目,选择 Node.js 最稳妥。
需要快速启动、内置工具链,且依赖已经在目标平台验证过的服务端或工具项目,可以选择 Bun。它不是浏览器,也不只是“更快的 Node.js”;它是带有 Node.js 兼容层和 Bun 专有 API 的独立运行时。
最终应将运行时写入项目的版本约束、CI 矩阵和发布产物,而不是只依赖开发者本机的默认命令。跨环境代码的正确性来自清晰的边界和目标环境验证,而不是偶然通过的本地运行。






