我把 Next.js 博客的开发、构建和 Docker 进程全部换成 Bun 后,代码里仍然有这些内容:
export const runtime = 'nodejs';
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation-node');
}还有 node:http、node:path 和 @types/node。乍看很矛盾:既然已经迁移到 Bun,为什么还保留一堆 Node 字样?
原因是这里混在一起的其实有三层:谁启动 JavaScript、框架按哪套服务器接口编译、业务代码调用哪些 API。
实际执行器看进程命令
开发脚本使用:
{
"scripts": {
"dev": "bun --bun next dev",
"build": "bun --bun next build",
"start": "bun --bun next start"
}
}Docker 使用:
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["bun", "server.js"]这两处决定实际执行器。bun --bun 会覆盖 CLI 文件里的 Node shebang;Bun 的运行时文档专门说明了这个行为。容器里的进程树则直接显示 tini -> bun server.js。
所以判断是不是 Bun,应该看命令和进程,不应该搜索源码里有没有 node:。
Next.js 的 runtime 是接口模型
Next.js Route Segment Config把 nodejs 作为默认 runtime。它表示路由可以使用完整的 Node.js API 模型和兼容 npm 包,而不是要求操作系统必须启动名为 node 的二进制文件。
Bun 的目标之一就是实现 Node.js API 兼容。Bun 官方说明会运行 Node.js 测试套件,并把“在 Node.js 可用、在 Bun 失败”的兼容问题视为 Bun bug。于是下面两件事可以同时成立:
- Next.js 按
nodejsruntime 构建路由。 - Bun 执行构建结果并提供兼容 API。
这和 Wine 运行 Windows API 程序有一点相似:程序面向哪套接口,与底层由哪个实现提供接口,不是同一个问题。
NEXT_RUNTIME 判断仍然有用
Next.js 的 instrumentation 文档明确使用 process.env.NEXT_RUNTIME 区分运行目标:
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation-node');
}
}我的 instrumentation-node.ts 会读取本地文件,还会接入 Next 内部 HTTP Server。条件导入的作用是阻止不兼容的编译目标提前加载它。
即使项目从不主动选择 Edge,这个判断也记录了模块边界。删掉判断不会让代码“更 Bun”,只会让环境限定变得不明确。
Edge 不是 Bun 的另一种写法
我一度想过把路由改成:
export const runtime = 'edge';但这和 Bun 迁移没有直接关系。Edge Runtime 使用受限的 Web API,不支持本地文件系统和完整 Node.js API;Next.js 的 Edge Runtime 文档列出了这些限制。我的博客在服务器启动时扫描 Markdown,附件也来自容器文件系统,不能只改一个常量就放到边缘平台。
Next.js 16.3.3 的 runtime 文档已经把 Route Segment 的 Edge Runtime 标为废弃,并建议移除 edge runtime 配置。就算部署平台仍支持旧入口,我也不会把它当成 Bun 迁移的目标。
真正的边缘迁移需要重新设计内容存储、附件、缓存、MCP 依赖和部署适配器。那是一项部署架构工作,不是字符串替换。
为什么保留 node:http
博客需要记录页面、静态资源、API 和 404 的完整访问日志。Next.js 没有在 Route Handler 层暴露所有这些请求,所以实现接入了它内部使用的 HTTP Server:
import {
Server,
type IncomingMessage,
type ServerResponse,
} from 'node:http';Bun 执行这段代码,并提供 node:http 兼容实现。把 API 路由交给 Hono 也不能替代它,因为 Hono 看不到 Next.js 页面和静态资源。
只有两个合理的删除条件:不再需要全站访问日志,或者不再由 Next.js 管理 HTTP Server。为了消除 import 而重写服务器,成本远大于收益。
为什么保留 node:path
内容加载要处理 Windows 和 Linux 路径、相对目录边界、扩展名和文件名。Bun 提供 fileURLToPath() 和 pathToFileURL(),但没有一套完整原生 API 覆盖 resolve()、relative()、dirname()、basename() 和 extname()。
继续使用 Bun 已兼容的 node:path,比自己实现一套跨平台路径算法可靠。Bun 的文件 I/O 文档也会在原生 API 未覆盖时直接使用 node:fs 和 path,并不要求“纯 Bun 代码”清空所有 Node 模块。
process.env 是框架接口
Next.js 用 process.env.NODE_ENV、process.env.NEXT_RUNTIME 和 process.env.NEXT_PUBLIC_* 处理构建期与运行期变量。把它们改成 Bun.env 可能让普通脚本更统一,却会离开 Next.js 的公开约定。
我的原则是:
- Bun 独占的本地脚本可以优先用
Bun.env。 - Next.js 明确规定的环境变量继续用
process.env。
同样,命令行脚本中的 process.exitCode = 1 也不必换成 Bun.exit(1)。前者设置最终退出状态,脚本仍有机会完成日志和清理;后者会立即退出,语义并不相同。
为什么 @types/node 不能删除
运行时和类型声明是两回事。安装 @types/bun 之后,Bun API 有了类型,但项目仍然使用 node:http,Next.js 自己也暴露 Node.js 服务器类型。
因此当前类型依赖是:
{
"devDependencies": {
"@types/bun": "^1.4.0",
"@types/node": "^26.4.0"
}
}删除 @types/node 不会让产物更小,因为它只是开发依赖;只会让类型检查失去声明。
我现在怎样判断要不要迁移
看到 Node API 时,我先问四个问题:
- Bun 是否有覆盖现有行为的稳定 API?
- 替换后是否真的更短、更清楚?
- Windows、Linux、测试和生产构建是否都能验证?
- 这个 API 是否属于 Next.js 的公开约定?
fs.readFile() 读取正文可以换成 Bun.file(),递归扫描可以换成 Bun.Glob,高精度计时可以换成 Bun.nanoseconds()。node:path 和 Next.js 的 process.env 则应该留下。
我不会再用“源码中还有多少个 node 字符串”衡量迁移完成度。更可靠的标准是:开发、构建、测试和生产进程由谁执行,保留的兼容层有没有明确用途,生产构建和容器是否真正跑过。
完整迁移步骤记录在 Next.js 16 博客完整迁移到 Bun 1.4,具体 API 取舍则见从 Node.js API 迁移到 Bun 原生 API。