Bun 实战:Windows 11 安装、运行时与包管理迁移指南

发布时间:2026/9/18 10:46:11
Bun 实战:Windows 11 安装、运行时与包管理迁移指南 上周帮同事排查一个前端项目npm install冷装跑了三分多钟改完一行样式本地 dev server 冷启动又是十几秒起步。我说你换个包管理器试试他第一反应是警惕我这项目跑得好好的为什么要动它。这个心态我特别理解因为我一开始也是这么想的——工具链这东西能用就别碰。但折腾了大半年之后我的结论是Bun 值得单独在机器上装一份但它不是那种装上就得把 Node.js 删掉的替代品。它更像一把随手能抽出来的瑞士军刀。Bun 是用 Zig 语言写成的 JavaScript / TypeScript 运行时同时把包管理器、打包器、测试运行器全部塞进了同一个可执行文件。bun install、bun run、bun test、bun build这几件事不需要再拼装 node npm vite jest 那一大套组合一个二进制就够。这篇内容写给两类人一类是听说过 Bun 但还没动手想搞清楚它到底能替掉手头哪些工具另一类是在 Windows 11 上装了半天被 PATH 或者命令找不到卡住过想把整个安装链路弄明白。我会从它凭什么快讲到我在什么场景下不敢用它把踩过的坑一个个摆出来。1. 为什么我在 Node.js 之外又装了一个 Bun1.1 一个二进制文件里塞了四件事大多数人第一次接触 Bun 是被快吸引的但真正让我留下来的是它的集成度。你装完 Bun 之后机器上多了bun这一个可执行文件它同时扮演四个角色角色对应命令替代的传统工具运行时bun run app.tsnode包管理器bun install/bun addnpm / yarn / pnpm测试运行器bun testjest / vitest打包器bun buildwebpack / esbuild / rollup这个组合的实际意义在于配置量的塌缩。传统 Node 项目要跑起来测试你得装 jest、装 ts-jest 或者 babel-jest、写jest.config.js、配transform。Bun 这边测试文件写好直接bun test就完事TypeScript 是原生认识的中间没有任何转译环节要你操心。我在一个内部工具项目上做过对比同样一份测试代码从 vitest 切到bun:test删掉了三个配置文件、两个 devDependency 和一段setupFiles。代码本身一行没改。这种少维护一堆配置的收益比单纯的启动速度更让我在意因为配置是会腐化的——半年后没人记得那个transformIgnorePatterns里的正则为什么那么写。1.2 JavaScriptCore 而不是 V8这意味着什么Bun 用的是 JavaScriptCore也就是 WebKit 那套引擎Node.js 用的是 V8。这个差异不是八卦它直接决定了你会在哪里遇到兼容性问题。V8 有一批独家的对外接口比如node:v8模块里的堆快照、序列化接口还有node:vm模块里那套在沙箱里执行代码的能力。这些东西 JavaScriptCore 没有对应实现Bun 只能选择自己补一部分、留一部分。所以如果你手头项目里有代码在调v8.serialize()做深拷贝优化或者拿vm.createContext()做模板沙箱切到 Bun 之前一定要先搜一遍代码库。反过来说JavaScriptCore 在一些启动路径上的开销确实更小这也是 Bun 冷启动看起来比较利索的原因之一。底层它还用 uWebSockets 做网络层在 Linux 上会尽量利用 io_uring 这类内核特性来减少系统调用次数。这些实现细节平时感知不到但当你把一个 Bun 写的 HTTP 服务压到几万 QPS 的时候差别就出来了。一句话总结Bun 不是 Node 的换皮它是重新实现的一套运行时好处是包袱轻代价是某些冷门 API 的覆盖度不如 Node 那么无死角。1.3 什么项目值得迁什么项目先放着我给自己定的规矩比较简单按风险从低到高排纯脚本和小工具直接上 Bun零风险。这类东西本来就没几个依赖Bun 原生跑 TypeScript 还能省掉 ts-node 那一层。新起的服务端项目可以直接用 Bun 打底尤其是用Bun.serve起服务、bun:sqlite存数据的轻量场景整个依赖树能瘦一大圈。既有前端工程建议只把包管理器换成 Bun构建和 dev server 还是留给 Vite 或框架自带的那套稳。有原生扩展依赖的项目比如用了 sharp、canvas、better-sqlite3 这类带.node二进制插件的包先在一个分支上跑通再说。重度依赖node:vm/node:v8的项目暂时别动。判断标准其实就一条这个项目里有多少依赖是必须跑在 Node 上的。如果答案是零那迁移成本基本只是改几条 npm script。2. Bun 在 Windows 11 上的安装从 PowerShell 到 PATH 的坑2.1 官方一键脚本与执行策略Windows 11 上最省事的方式是 PowerShell 一行命令powershell -c irm bun.sh/install.ps1 | iex这条命令做的事是从网络拉取安装脚本内容然后在当前会话里直接执行不走脚本文件落盘那一步。这一点很关键因为 Windows 的 ExecutionPolicy 限制的是.ps1 文件的执行而iex执行的是内存里的字符串所以哪怕你的策略是 Restricted这条命令一般也能过。真正常见的拦路虎是这几类公司电脑的受限语言模式。有些企业环境会把 PowerShell 锁在 ConstrainedLanguage 模式这种模式下连基本的 .NET 调用都被禁止脚本跑一半会报方法调用失败。这种情况别硬刚直接走下面的 npm 安装路径。网络不可达。命令执行后一直卡着没反应大概率是拿不到脚本内容。可以先单独跑一句irm bun.sh/install.ps1看看能不能打印出脚本正文能打印说明网络通了再去iex。杀毒软件拦截。有些安全软件会对下载内容并立即执行这个行为模式报警这时候手动下载脚本、大致扫一眼内容再执行会更稳妥也算是个好习惯。提示irm xxx | iex这种模式在任何操作系统上都存在下载即执行的风险建议第一次装的时候先把脚本内容拉下来看一眼确认没有奇怪的操作再执行。2.2 装完之后命令找不到PATH 排查顺序安装脚本跑完会提示bun was installed successfully但很多人新开终端敲bun --version还是报不是内部或外部命令。这不是装失败是 PATH 没生效。按下面的顺序排查基本三步之内能定位第一步确认文件真的在。默认安装目录在用户目录下的.bun\bin去文件资源管理器地址栏输入%USERPROFILE%\.bun\bin回车看有没有bun.exe。如果有说明安装成功问题纯粹在环境变量。第二步确认 PATH 里有没有这条。打开系统属性 → 高级 → 环境变量在用户变量里找Path双击进去看列表里有没有%USERPROFILE%\.bun\bin。安装脚本一般会自动加但如果你当时用管理员权限跑过、或者之前手动改过 Path 内容可能就漏了。没有就手动加一条。第三步也是最容易被忽略的一步重开终端。Windows 的环境变量是在进程启动那一刻读取的已经开着的 PowerShell、cmd、VS Code 集成终端全都不认新加的变量。注意是重开终端不是重启电脑很多人在这里白白重启了两三次机器。还有一个坑值得单独提醒如果你之前用npm install -g bun装过一次现在又用官方脚本装了一次机器上会有两个 bun.exe一个在 npm 全局目录一个在.bun\bin。这时候bun --version出来的是哪个完全取决于 PATH 里两条路径谁在前面。排查版本对不上的问题时用where.exe bun看一眼有几个、分别在哪比猜快得多。2.3 npm 全局装法与 Scoop 装法怎么选三条安装路径各有适用场景我列个对照安装方式命令适合谁需要注意官方脚本irm bun.sh/install.ps1 | iex个人机器想要自更新需要能访问外部网络npm 全局npm install -g bun已有 Node 环境公司管控严升级走 npmbun upgrade行为会不同Scoopscoop install bun习惯用 Scoop 管理工具的人版本更新跟随 Scoop 的 bucket我个人倾向官方脚本因为bun upgrade能直接把自身升到最新版不用绕包管理器。但公司内网机器我一般用 npm 那套理由很简单既然 Node 已经在白名单里走它的通道阻力最小。关于 Scoop如果你的开发机上已经有一堆 CLI 工具是它管的比如 jq、fd、ripgrep那把 Bun 也交给它统一管理升级命令一致不容易忘。缺点是 bucket 里的版本更新会有延迟新特性可能要等几天。2.4 验证与升级装完之后建议按顺序跑一遍这几条确认整条链路是通的bun --version bun --help bun upgrade第一条看版本号第二条确认命令本身可执行第三条测自更新通道。如果你是用官方脚本装的bun upgrade会去拉最新版并覆盖当前二进制如果是 npm 装的它会走 npm 的全局升级流程。有个小细节bun upgrade在 Windows 上有时候会因为当前有bun run进程在跑而失败提示文件被占用。这时候把正在跑的 watch 进程关掉再执行就行。这个报错信息不太直观我第一次遇到时以为是权限问题白折腾了十分钟。3. 把 Bun 当运行时用第一个脚本和它省掉的配置3.1 bun init 生成的文件逐个看在一个空目录里执行bun init会进入一个交互式问答问项目名和入口文件名。想快速跳过的话直接加-ybun init -y生成的东西不多逐个说清楚package.json和 npm 的那个是同一个格式Bun 完全按标准来不会给你塞私有字段。scripts里默认会有start: bun run index.ts。index.ts入口文件。注意后缀是.ts不是.js因为它默认就用 TypeScript。tsconfig.json这个文件是重点。它继承的是 Bun 提供的类型定义包里面配置了moduleResolution: bundler这类对 Bun 友好的选项。具体继承的是bun-types还是types/bun不同版本不太一样跑一次bun init看它生成什么最准。README.md没什么可说。.gitignore早期版本会生成新版本有时候不生成如果没有记得自己补一个至少要忽略node_modules。整个初始化过程不到一秒没有npm install那种等待因为 Bun 的项目初始形态确实不需要装任何依赖——它自己就是运行时、是测试框架、是打包器。这种开箱零依赖的体验第一次用会觉得有点不真实。3.2 原生跑 TypeScript 与 TSX不用配 loader这是 Bun 最没有争议的优势。传统流程里你想在 Node 上跑一个.ts文件需要 ts-node 或者 tsx还得配tsconfig.json里的module、target、esModuleInterop一堆选项配错了就报Cannot use import statement outside a module。在 Bun 里就是bun run index.ts bun run src/cli.tsx.tsx也一样原生支持不需要额外的 JSX 转译配置。Bun 内部走的是自己实现的转译器边读边转不生成中间产物落盘。注意Bun 的转译器只负责能跑起来它不做类型检查。也就是说const x: number abc这种错误Bun 照样能跑不会拦你。类型检查这件事必须交给tsc --noEmit或者编辑器。我见过有人以为换了 Bun 就不需要 tsc 了结果一路写错到运行时报错才发现这个坑挺典型的。我的做法是在package.json里单独留一条{ scripts: { typecheck: tsc --noEmit, dev: bun run --watch src/index.ts } }本地开发用bun run --watch自动重启提交前跑一次bun run typecheck。两条腿走路缺一不可。3.3 .env 自动加载与它的生效范围Bun 会自动读取项目根目录下的.env、.env.local、.env.development这类文件把它塞进process.env。这件事不需要dotenv包也不需要--env-file参数。这带来一个便利和一个隐患。便利是省掉了一个依赖和一行import dotenv/config隐患是加载是隐式的——你在 Bun 里写process.env.API_KEY能拿到值但同样的代码挪回 Node 就变成undefined。如果你的项目需要双运行时兼容建议还是显式import dotenv/config让行为不依赖运行环境。另外.env的优先级顺序在不同版本有过调整如果你有多个 env 文件同时存在最稳妥的办法是打一行日志确认console.log(Object.keys(process.env).filter(k k.startsWith(APP_)));跑一次看到底哪个文件的值胜出比翻文档可靠。这个习惯我保留到现在因为 env 覆盖问题排查起来特别费时间。3.4 用 Bun.serve 起一个不依赖框架的服务Bun 内置了 HTTP 服务端写法比 Node 的http.createServer简洁得多const server Bun.serve({ port: 3000, fetch(req) { const url new URL(req.url); if (url.pathname /health) { return new Response(JSON.stringify({ ok: true }), { headers: { content-type: application/json }, }); } return new Response(Not Found, { status: 404 }); }, }); console.log(listening on ${server.port});这段代码用bun run server.ts直接跑不需要任何依赖。fetch的签名和浏览器里的 Fetch API 基本一致路由匹配自己写switch或者URL判断都行。配套启动命令推荐用热重载模式bun --hot server.ts--hot和--watch的区别值得说清楚--watch是检测到文件变化后重启进程全局变量会丢--hot是在不重启进程的前提下替换模块Bun.serve这类对象的连接状态能保住改完代码不用重新建立连接。做本地开发的时候--hot体验明显好一截但要注意它的作用范围主要是 Bun 自己的模块系统如果你用了外部框架不一定完全生效。4. bun install 的速度到底来自哪里4.1 全局缓存加硬链接省掉的是哪一步 IO很多人只知道 Bun 装包快但说不清快在哪。拆开看其实就四件事第一进程启动开销小。用 Zig 写的二进制启动没那么多初始化流程。这一点在 CI 里跑几十次 install 的时候累积效应明显。第二下载是并行的。解析出依赖树之后所有需要下载的 tarball 会并发拉取而不是一个个排队。第三全局缓存。所有下载解压过的包会存在~/.bun/install/cache里。你机器上有十个项目都依赖同一个版本的 lodash这个包只会下载解压一次后面九个项目直接命中缓存。第四也是最能省 IO 的一步——硬链接。从缓存往node_modules里放文件的时候Bun 用的是硬链接而不是复制。硬链接的本质是同一份数据挂两个名字磁盘上只有一份内容几乎不产生额外 IO。对比之下npm 那种复制方式的耗时是实打实的字节搬运。这里有个很容易踩的坑硬链接要求源和目标在同一个文件系统/分区上。如果你把缓存放在 C 盘默认位置项目放在 D 盘跨分区的情况下硬链接无法建立Bun 会退化成复制速度优势会缩水不少。解决办法是设置环境变量把缓存挪到同一个盘# Windows PowerShell $env:BUN_INSTALL_CACHE_DIR D:\bun-cache # Linux / macOS export BUN_INSTALL_CACHE_DIR/data/bun-cache这个细节在文档里不会特别强调但对项目在 D 盘、缓存在 C 盘的 Windows 用户来说收益相当直观。4.2 锁文件从 bun.lockb 到 bun.lock早期 Bun 的锁文件叫bun.lockb是二进制格式。二进制的好处是解析极快、体积小坏处是代码评审时看不了 diff——Git 里显示Binary files differ谁改了什么依赖完全看不出来。这在团队协作里是个真问题。后来 Bun 引入了文本格式的bun.lock现在默认生成的基本都是它。两种格式的对比特性bun.lockbbun.lock格式二进制文本解析速度极快快Git diff 可读否是代码评审友好差好是否仍支持是是如果你在老项目里看到bun.lockb想迁移到文本格式直接删掉旧的锁文件重新bun install就会生成新的。不过团队里一定要统一不能一半人提交.lockb、一半人提交.lock否则每次拉代码都会看到锁文件互相覆盖。4.3 和 npm、pnpm 混装时的依赖树冲突Bun 的包管理器能读 npm 的package-lock.json、yarn 的yarn.lock、pnpm 的pnpm-lock.yaml第一次bun install的时候会自动识别并转换。这个兼容性设计很贴心但混用会出问题。最典型的场景是项目里同时存在package-lock.json和bun.lock。这时候你跑npm installnpm 按自己的锁文件装一遍然后跑bun installBun 又按自己的锁文件校验一遍两边互相认为对方把依赖搞乱了。表现出来就是依赖版本反复横跳、node_modules里出现重复包。我的处理原则很直接一个项目只保留一个锁文件其他全删掉并在 README 里写清楚用哪个包管理器。如果是从 npm 迁到 Bun步骤是rm -rf node_modules package-lock.json bun install跑完之后确认bun.lock生成正常再提交。这一步做完后续就干净了。还有一点Bun 对.npmrc的支持是逐步补齐的企业私有源、scope 映射这类配置建议在迁移后跑一次真实的安装验证别假设所有.npmrc项都被识别了。5. 测试与打包bun test 和 bun build 能顶多少活5.1 从 Jest 搬测试时哪些写法要改bun test的 API 设计明显是奔着 Jest 兼容去的describe、it、expect、beforeEach、toBe、toMatchSnapshot这些名字全对得上。文件匹配规则也宽松*.test.ts、*_test.ts、*.spec.ts、*_spec.ts都能被自动发现。迁移时改的主要是导入import { test, expect, describe, mock, spyOn } from bun:test;Jest 那套全局注入的写法要改成显式导入。真正会卡住的是两个地方模块级 mock。Jest 里写jest.mock(./db)会自动把整个模块替换成 auto-mock。Bun 里对应的是mock.module()但行为不完全一致尤其涉及 ESM 循环依赖的时候。我的建议是别依赖 auto-mock把需要替换的部分抽成可注入的函数参数测试里传假实现。这个改法虽然多写几行但换任何框架都不受限。计时器控制。Jest 的jest.useFakeTimers()在 Bun 里有对应实现但如果你代码里同时用了Date.now()和setTimeout两者的推进节奏要对齐。遇到测试偶发失败的情况先怀疑这里。实测下来的收益一个原本跑 4 秒左右的测试套件bun test跑完大约 1.2 秒。差距主要来自启动开销——Jest 每次都要走一遍转译和模块加载流程Bun 这边基本是直接跑。5.2 bun build 的入口、目标与产物bun build是一个通用打包器命令行用法bun build ./src/index.ts --outdir./dist --targetbrowser --minify --sourcemap几个关键参数的含义--target决定内置模块和全局变量的替换策略。browser会尽量按浏览器环境处理node会保留 Node 内置模块的外部引用bun则按 Bun 自己的运行时处理。--outdir输出目录。--minify压缩包含去空白、变量名缩短、语法优化。--splitting开启代码分割生成多个 chunk只在browser和bun目标下生效。--external把某个包标记为外部依赖不打进产物。入口文件不限于 JavaScript/TypeScript。CSS 可以作为入口直接打包HTML 文件也能作为入口Bun 会顺着里面的script和link把相关资源一起处理。这个能力让它在做小型静态页面构建时可以完全取代一套 webpack 配置。我拿它和 esbuild 做过一次粗略对比同一个中型项目产物体积和构建时间基本在同一量级Bun 的启动略快一点。但生态上 Bun 的插件体系不如 esbuild 成熟如果你有大量自定义 loader 需求还是 esbuild 更顺手。5.3 用 --compile 把脚本变成单文件可执行程序这是我认为 Bun 最有意思的能力值得单独拿出来讲。它能把一个 TypeScript 脚本编译成一个独立的可执行文件目标机器上不需要装 Bun、不需要装 Nodebun build ./cli.ts --compile --outfilemycli产物是一个单一文件双击或者在终端里直接运行就行。更实用的是它支持交叉编译bun build ./cli.ts --compile --targetbun-windows-x64 --outfilemycli.exe bun build ./cli.ts --compile --targetbun-linux-x64 --outfilemycli在 macOS 上就能同时产出 Windows 和 Linux 的二进制。我用这个方式把几个内部运维脚本做成了单文件工具发给同事之后不用再交代你先装个 Node 再装个 ts-node这种前置条件直接跑。需要注意的边界体积。产物里打包了整个 Bun 运行时一个简单的 CLI 出来大概几十 MB。分发脚本够用了但如果是要塞进容器镜像的小工具这个体积得掂量。动态导入。代码里如果用了运行时动态计算出来的模块路径打包器无法静态分析会漏掉。这种情况得用--external或者改成静态导入。原生依赖。如果依赖里有.node二进制处理起来会麻烦跨平台编译基本别想。6. 我在迁移时踩到的四个具体问题6.1 原生扩展模块加载失败现象项目bun install一切正常跑起来报错提示某个模块Cannot find module或者直接段错误。根因原生扩展native addon是用 C/C 写的.node二进制编译时绑定了特定运行时的 ABI。有些包只发布了针对 Node 预编译的版本Bun 的 N-API 兼容层能覆盖相当一部分但不是全部。特别是那些依赖nan一个较老的抽象层而不是 N-API 的包很容易出问题。排查链路先确认是哪个包。报错栈里往上找第一个不是node_modules/.bun路径的帧。看这个包的package.json里有没有gypfile: true或者binding.gyp。如果它同时提供纯 JS 的 fallback 实现看看能不能通过条件导出切过去。实在不行就在那个项目里保留 Node 跑业务逻辑把这个包相关的功能拆成独立进程用 Node 启动Bun 这边通过 IPC 调用。这个方案听起来绕但比死磕兼容性划算得多。我现在的判断标准是如果一个项目能因为一个原生扩展卡住超过两小时就直接放弃全量迁移用混合方案。6.2 node_modules 布局差异导致的模块解析异常现象代码里能import到的包实际运行时报Cannot find module或者同一个包被加载了两份实例instanceof判断失效。根因Bun 生成node_modules的布局和 npm、pnpm 都有差异。pnpm 用的是符号链接加全局 store 的方案对幽灵依赖卡得很严npm 是扁平化提升Bun 倾向于尽可能贴近 npm 的扁平结构但并不完全一样。在 monorepo 里如果 workspace 之间互相引用解析规则差异会被放大。排查方式bun pm ls --all | head -50先看清实际的依赖树。然后针对可疑的包确认磁盘上有几份拷贝。如果同一个包的同一个版本出现了两份路径说明提升策略没对齐。处理办法在package.json里显式声明所有被间接使用的依赖别依赖提升带来的免费午餐。这是好习惯无论用哪个包管理器都应该这么做。另外 monorepo 里尽量统一用 Bun 的 workspaces不要一边 Bun 一边 pnpm。6.3 Windows 上的符号链接与路径大小写现象在 Windows 上跑bun install报权限错误或者创建链接失败。根因有两个因素叠加。一是 Windows 创建符号链接默认需要管理员权限或者开发者模式二是 NTFS 虽然默认大小写不敏感但 Git 和某些工具会保留大小写。当代码里写import x from ./Utils而实际文件名是utils.ts时在 macOS 上能跑在 Windows 上也能跑但一旦涉及硬链接和缓存路径的比对就可能出现找不到文件。处理方式打开 Windows 的开发者模式设置 → 系统 → 开发者选项这样创建符号链接不再需要管理员权限。检查项目里存不存在只有大小写差异的同名文件。这种情况在 Git 上会引发诡异问题git config core.ignorecase false之后git status能看出来。如果实在绕不过可以把 Bun 的缓存目录显式设到项目所在盘减少跨盘操作。6.4 CI 上必须加 --frozen-lockfile现象本地跑得好好的CI 上装出来的依赖版本和本地不一样测试莫名其妙挂了。根因CI 上跑的是bun install如果锁文件和package.json有出入Bun 会重新解析依赖树并更新锁文件——也就是说CI 悄悄用了一套和本地不同的依赖版本。处理方式CI 里的安装命令改成bun install --frozen-lockfile这个参数的作用是一旦锁文件和package.json对不上直接报错退出而不是静默更新。这样问题会在 CI 阶段就暴露出来而不是等到线上。配套的一条是锁文件必须提交到版本库。这点看起来是常识但因为 Bun 的锁文件经历了从二进制到文本的切换期有些项目在迁移过程中把它加进了.gitignore然后就一直没加回来。这类问题排查起来特别费劲因为本地永远是对的。7. 我现在的工具链把 Bun 放在哪一格7.1 日常开发的分工方式折腾到现在我的分工大概是这样的场景用什么理由依赖安装Bun速度优势明显兼容 npm 生态一次性脚本、数据处理Bun原生 TS不用配环境新服务端项目Bun一体化依赖树小既有前端工程构建Vite / 框架自带生态成熟插件丰富需要原生扩展的服务Node兼容性优先CI 里的测试Bun启动快Jest 语法兼容这个组合的核心思路是把 Bun 用在边界清晰、失败影响可控的位置。包管理和脚本执行这两块就算出问题也容易回退——删掉bun.lock、重新npm install就回去了。但构建链路和生产服务不一样一旦踩坑排查成本高所以我对这两块相对保守。bunfig.toml是个值得一提的配置文件放在项目根目录可以覆盖安装源、缓存策略、测试匹配规则这些。如果你的团队有私有 registry 或者特殊的测试文件命名习惯这个文件比命令行参数更好维护因为它是跟着项目走的。7.2 三个能立刻感受到提速的场景最后说三个我个人觉得收益最直观的场景你可以照着试第一个是 CI 缓存全废之后的冷装。有些 CI 环境的缓存经常命中不了这时候bun install和npm install的时间差能有三到五倍。这种场景下迁移几乎是纯赚。第二个是写一次性数据处理脚本。比如读一个 CSV、清洗完写回 JSON。以前要么写.mjs然后忍受没有类型提示要么配一套 ts-node。现在bun run script.ts直接跑写的时候编辑器有类型跑的时候没有额外配置。第三个是给非技术同事做小工具。用--compile打成单文件对方双击就能用不用解释什么是 Node.js。这个场景的价值不在技术层面而在省掉的沟通成本。至于哪个场景更适合你我的建议是先从最不重要的那个项目试起。找一个小工具项目切过去跑一周感受一下出错频率和排查难度再决定要不要往核心项目推。工具选型这件事别人的体验只能作为参考真正的判断标准永远是你自己那套代码库的依赖构成。