Deno 2.9 深度实战:当「deno desktop」让 Web 开发者直捣桌面端——从架构到性能,一次把 JS 运行时第三极讲透(2026)
一、引言:JavaScript 运行时三足鼎立,Deno 凭什么站上牌桌?
2018 年,Ryan Dahl(没错,就是 Node.js 的发明人)在一场演讲里抛出了一个注定载入史册的「后悔清单」——他列举了 Node.js 设计中的十大错误,比如用 node_modules 造了依赖地狱、没有原生 TypeScript 支持、安全模型形同虚设……然后他宣布:我要造一个新东西来「矫正」这一切。
这个新东西叫 Deno。
时间快进到 2026 年 7 月。Node.js 依然是后端 JavaScript 的绝对王者,Bun 凭借 Zig 实现的极速体验成了搅局者,而 Deno——它刚刚发布了 2.9 版本,带来了一个可能让整个桌面开发格局松动的功能:deno desktop。
在深入代码之前,我们先看清一个问题:为什么开发者还要在意 Deno?
1.1 从「颠覆者」到「第三极」
这些年 JavaScript 运行时经历了三个阶段:
| 阶段 | 代表 | 核心理念 |
|---|---|---|
| 1.0:让 JS 跑在服务器 | Node.js | 事件循环 + CommonJS |
| 2.0:更安全、更现代 | Deno | 原生 TS + 安全权限 + ESM |
| 2.5:极致性能 | Bun | Zig 实现 + 内置打包器/测试器/运行器 |
Deno 的定位一直很清晰:不是要成为最快的运行时(那是 Bun 的战场),也不是要维护最大生态(那属于 Node),而是要成为 最现代、最安全、最佳开发者体验 的运行时。
2026 年的 Deno 2.9 把这条路线推到了一个新高度:它不仅完善了 Node.js 兼容性,更用 deno desktop 开辟了一个全新的战场——用 Web 技术写桌面应用,但不需要 Electron 的「染色体级肥胖」。
1.2 为什么现在值得认真看 Deno?
四个理由:
- Node.js 兼容性终于成熟了 — Deno 2.8/2.9 把兼容目标锁定在 Node.js 26,npm 包的使用体验已经逼近原生
- deno desktop 是一个范式突破 — 一个命令把 Web 项目变成桌面二进制,不需要 WebView 打包器、不需要额外的构建工具链
- 性能反超 — 2.9 的冷启动比 2.8 快 2 倍,内存占用在高负载下稳定在 62MB(相比 2.8 的 197MB,降低了 3.1 倍)
- 生态建设加速 — Fresh Web 框架、deno.land/x 包管理、JSR(JavaScript Registry)标准化注册表
在这篇文章里,我会从 架构原理 → 代码实战 → 性能基准 → 迁移指南 四个维度,把 Deno 2.9 讲透。
二、Deno 的架构哲学:和 Node.js 有什么本质不同?
在深入 2.9 之前,有必要先理解 Deno 的底层设计。这不是「另一个运行时」,而是从底到上有着根本不同。
2.1 V8 + Rust + Tokio 的三层架构
┌────────────────────────────────────────┐
│ TypeScript / JavaScript │
├────────────────────────────────────────┤
│ Deno Runtime 层 │
│ ┌──────┬──────┬──────┬────────────┐ │
│ │ TS │ URL │ 权 │ Web │ │
│ │ 编译 │ 导入 │ 限 │ 标准 API │ │
│ └──────┴──────┴──────┴────────────┘ │
├────────────────────────────────────────┤
│ Rust (op layer) │
│ 文件系统 | 网络 | 子进程 | KV 存储 │
├────────────────────────────────────────┤
│ Tokio (异步运行时) │
├────────────────────────────────────────┤
│ V8 JavaScript 引擎 │
└────────────────────────────────────────┘
对比 Node.js 的 C++ + libuv 架构,Deno 用 Rust 实现了所有底层操作(Deno 称之为 "ops"),通过 Tokio 处理异步 I/O。这种设计的优势:
- Rust 的内存安全保证 — 没有 C++ 的野指针和缓冲区溢出问题
- Tokio 的零成本抽象 — 异步 I/O 的开销极低
- 统一的 op 层 — 所有系统调用都通过清晰的边界,便于审计和安全控制
2.2 权限模型:从「全有或全无」到「按需授予」
Deno 最容易被低估的设计就是它的权限模型。想想看,一个 npm 包 left-pad 就有权限读取你服务器上的 /etc/passwd——这在 Deno 里不可能发生。
// Deno 下,默认没有权限
// 这将抛出一个 PermissionDenied 错误
const data = await Deno.readTextFile("/etc/passwd");
// 运行时需要显式授权
// deno run --allow-read=/etc/passwd app.ts
// 或运行时请求权限
const perm = await Deno.permissions.request({ name: "read", path: "/etc/passwd" });
if (perm.state === "granted") {
const data = await Deno.readTextFile("/etc/passwd");
}
Deno 2.9 细化了权限级别,支持:
--allow-read=/path— 只读某个路径--allow-write=/path— 只写某个路径--allow-net=example.com— 只允许特定域名--allow-env=HOME,PATH— 只访问特定环境变量--allow-sys=hostname,osRelease— 只允许特定系统信息
这对生产环境的意义巨大:你可以给一个 CI 脚本仅开放写 /tmp/output 的权限、同时只允许连接 api.github.com——即使脚本被注入恶意代码,它的破坏范围也被限制在极小的沙箱里。
2.3 原生 ESM 和 TypeScript
Deno 直接从 1.0 就强制使用 ES Modules(没有 CommonJS 的兼容包袱),并且内置 TypeScript 编译器。
// 原生 ESM 导入
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
// 直接写 TypeScript,不需要 tsconfig.json
interface User {
id: number;
name: string;
email: string;
}
const user: User = await fetchUser(42);
Deno 2.9 引入了 锁文件跨平台迁移(cross-lockfile migration),这意味着你可以将 package-lock.json、yarn.lock 或 pnpm-lock.yaml 一键迁移到 Deno 的 deno.lock:
# 将 Node.js 项目的依赖迁移到 Deno
deno install --entrypoint src/main.ts --lock deno.lock --lock-write
# 自动从 package.json 读取依赖
deno install
三、Deno 2.9 核心新特性深度拆解
3.1 deno desktop:一个命令,Web 项目秒变桌面应用
这是 2.9 的最大亮点。我们先看它的使用方式:
# 最简单的用法
deno desktop run app.ts
# 指向一个 Fresh 项目
deno desktop run --allow-net --allow-read ./main.ts
# 编译为独立的桌面二进制
deno desktop compile --allow-net --allow-read main.ts
# 输出: myapp (macOS .app / Windows .exe / Linux 二进制)
这就是一个完整的桌面应用了。UI 层在系统 WebView 中运行,逻辑层在 Deno 运行时中执行,最终打包成一个独立的二进制文件。
架构对比:deno desktop vs Electron vs Tauri
| 特性 | deno desktop | Electron | Tauri 2.x |
|---|---|---|---|
| 打包体积 | ~30MB(含 Deno 运行时) | ~150MB(含 Chromium) | ~3MB(系统 WebView) |
| 内存占用 | 起步 ~35MB | 起步 ~120MB | 起步 ~15MB |
| 渲染引擎 | 系统 WebView | 内置 Chromium | 系统 WebView |
| 开发语言 | JS/TS/HTML/CSS | JS/TS/HTML/CSS | Rust + 前端框架 |
| API 访问 | Deno API + Web API | Node.js + Electron API | Rust Command + 前端 |
| 跨平台 | macOS/Windows/Linux | macOS/Windows/Linux | macOS/Windows/Linux |
| 安装包大小 | ~30MB | ~150MB+ | ~3MB |
关键区别在于:
Electron 打包了整个 Chromium,所以物理体积巨大,但好处是渲染一致性——你不需要操心不同操作系统的 WebView 差异。
Tauri 追求极致小体积,但要求你必须会 Rust 来编写后端命令。如果你的团队是纯 Web 技术栈,Tauri 有学习门槛。
deno desktop 走了一条中间路线:打包的是 Deno 运行时(~30MB),后端逻辑全用 TypeScript 写,不需要学 Rust。如果你已经是 Deno 用户,学习成本几乎为零。
底层实现解析
deno desktop 的架构如下:
┌──────────────────────────────────────────┐
│ WebView (系统原生) │
│ macOS: WKWebView │
│ Windows: WebView2 │
│ Linux: WebKitGTK │
├──────────────────────────────────────────┤
│ IPC Bridge │
│ (Deno op → WebView postMessage) │
├──────────────────────────────────────────┤
│ Deno Runtime (Worker) │
│ TypeScript 业务逻辑 + 文件/网络/KV API │
│ ┌───────────────┐ ┌──────────────────┐ │
│ │ deno compile │ │ 资源文件内嵌 │ │
│ └───────────────┘ └──────────────────┘ │
├──────────────────────────────────────────┤
│ ~30MB 单一二进制 │
└──────────────────────────────────────────┘
核心机制基于 deno compile 的 same-process 架构。当运行 deno desktop compile 时:
- Deno 将 TypeScript 源码编译为 V8 快照
- 所有静态资源(HTML/CSS/图片)按指定路径嵌入二进制
- 启动时创建一个 WebView 实例
- WebView 通过 IPC 与 Deno Worker 通信
- Deno Worker 处理文件系统、网络请求、数据库等后端逻辑
- WebView 仅负责 UI 渲染
这意味着你可以在桌面应用中直接使用 Deno 的所有 API——文件系统、网络、KV 存储、crypto……而不需要额外的桥接层。
实战:构建一个 Markdown 笔记桌面应用
让我们用 deno desktop 从零构建一个本地 Markdown 编辑器:
// main.ts — Deno 桌面笔记应用
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
// Markdown 解析库
import { render } from "npm:marked";
// 1. 提供 HTTP 服务给 WebView 渲染 UI
const handler = async (req: Request): Promise<Response> => {
const url = new URL(req.url);
if (url.pathname === "/") {
const html = await Deno.readTextFile("./public/index.html");
return new Response(html, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
if (url.pathname === "/api/notes" && req.method === "GET") {
const notes = await listNotes();
return Response.json(notes);
}
if (url.pathname === "/api/notes" && req.method === "POST") {
const { title, content } = await req.json();
const note = await saveNote(title, content);
return Response.json(note, { status: 201 });
}
if (url.pathname === "/api/preview" && req.method === "POST") {
const { content } = await req.json();
const html = render(content);
return new Response(html, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
return new Response("Not Found", { status: 404 });
};
// 用 Deno KV 做本地持久化(2.9 已稳定)
const kv = await Deno.openKv("./notes.db");
interface Note {
id: string;
title: string;
content: string;
createdAt: number;
updatedAt: number;
}
async function listNotes(): Promise<Note[]> {
const notes: Note[] = [];
for await (const entry of kv.list<Note>({ prefix: ["notes"] })) {
notes.push(entry.value);
}
return notes.sort((a, b) => b.updatedAt - a.updatedAt);
}
async function saveNote(title: string, content: string): Promise<Note> {
const id = crypto.randomUUID();
const now = Date.now();
const note: Note = { id, title, content, createdAt: now, updatedAt: now };
await kv.set(["notes", id], note);
return note;
}
console.log("📝 笔记应用启动,端口: 3000");
await serve(handler, { port: 3000 });
对应的 HTML 界面(放在 public/index.html):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deno Notes</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: grid;
grid-template-columns: 300px 1fr;
height: 100vh;
}
.sidebar {
background: #1a1a2e;
color: #eee;
padding: 16px;
overflow-y: auto;
}
.sidebar h1 { font-size: 18px; margin-bottom: 16px; }
.note-item {
padding: 12px;
background: rgba(255,255,255,0.05);
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
}
.note-item:hover { background: rgba(255,255,255,0.1); }
.editor {
display: flex;
flex-direction: column;
padding: 20px;
}
#title-input {
font-size: 24px;
font-weight: bold;
border: none;
outline: none;
padding: 8px 0;
margin-bottom: 16px;
border-bottom: 2px solid #e0e0e0;
}
#content-textarea {
flex: 1;
border: none;
outline: none;
font-size: 14px;
line-height: 1.8;
resize: none;
font-family: 'SF Mono', 'Fira Code', monospace;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
button {
padding: 8px 16px;
border: none;
background: #4361ee;
color: white;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
button:hover { background: #3a56d4; }
</style>
</head>
<body>
<div class="sidebar">
<h1>📝 Deno Notes</h1>
<button onclick="newNote()">+ 新建笔记</button>
<div id="note-list"></div>
</div>
<div class="editor">
<div class="toolbar">
<button onclick="saveNote()">💾 保存</button>
<button onclick="preview()">👁️ 预览</button>
</div>
<input id="title-input" placeholder="输入标题..." />
<textarea id="content-textarea" placeholder="开始写 Markdown..."></textarea>
<div id="preview-area" style="display:none;"></div>
</div>
<script>
let currentNoteId = null;
async function loadNotes() {
const res = await fetch('/api/notes');
const notes = await res.json();
const list = document.getElementById('note-list');
list.innerHTML = notes.map(n => `
<div class="note-item" onclick="openNote('${n.id}')">
<strong>${n.title || '无标题'}</strong>
<div style="font-size:11px;color:#888;margin-top:4px">
${new Date(n.updatedAt).toLocaleString()}
</div>
</div>
`).join('');
}
async function newNote() {
currentNoteId = null;
document.getElementById('title-input').value = '';
document.getElementById('content-textarea').value = '';
document.getElementById('preview-area').style.display = 'none';
document.getElementById('content-textarea').style.display = 'block';
}
async function openNote(id) {
currentNoteId = id;
const res = await fetch('/api/notes');
const notes = await res.json();
const note = notes.find(n => n.id === id);
if (note) {
document.getElementById('title-input').value = note.title;
document.getElementById('content-textarea').value = note.content;
}
}
async function saveNote() {
const title = document.getElementById('title-input').value;
const content = document.getElementById('content-textarea').value;
await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content })
});
await loadNotes();
}
async function preview() {
if (document.getElementById('preview-area').style.display === 'block') {
document.getElementById('preview-area').style.display = 'none';
document.getElementById('content-textarea').style.display = 'block';
return;
}
const content = document.getElementById('content-textarea').value;
const res = await fetch('/api/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
const html = await res.text();
document.getElementById('preview-area').innerHTML = html;
document.getElementById('preview-area').style.display = 'block';
document.getElementById('content-textarea').style.display = 'none';
}
loadNotes();
</script>
</body>
</html>
运行:
deno desktop run --allow-read --allow-write --allow-net --allow-env main.ts
或者编译为独立桌面应用:
deno desktop compile --allow-read --allow-write --allow-net main.ts
酷吧?你的 Web 应用变成了桌面应用。没有 Electron,没有额外的打包配置。这背后是 deno desktop 的运行时嵌入和系统 WebView 复用机制。
3.2 启动速度:从 34ms 到 17ms 的「战争」
Deno 2.9 最大的工程成就之一,是把冷启动时间砍掉了一半。
这看着像是「无聊的性能优化」,但在 Serverless 和 Edge 计算场景下,启动时间直接决定了响应延迟。想想 Lambda 的冷启动——每节省 10ms,就是百万级请求下的显著成本降低。
怎么做到的呢?四个技术手段:
a) 延迟加载 node: 全局变量
Deno 2.8 在启动时预加载了所有 Node.js 兼容模块的全局变量 —— process、Buffer、require……即使你只用 Deno 原生 API,这些变量也要被初始化,这占据了快照体积的很大一部分。
Deno 2.9 改为:只在真正用到 node: 前缀导入时才加载这些变量。如果纯用 Deno.serve(),Node 兼容层根本不加载。这个改动将快照体积削减了约 40%。
b) Node 引导程序按需执行
类似地,Node.js 的兼容引导程序(bootstrapper)之前是在所有 Worker 中都执行的。现在 Deno 2.9 只在实际创建了 Node Worker 时才运行它。对于普通的 HTTP 服务 Worker,这段初始化代码完全不跑。
c) V8 代码缓存
对于剩余的延迟加载 ESM 模块,Deno 2.9 启用了 V8 的代码缓存机制。V8 编译过的代码会被缓存到磁盘,下次启动时直接载入编译后的字节码,跳过解析和编译阶段。
V8 编译管线对比:
┌─────────────┐ ┌──────────────┐ ┌──────────┐
│ 源码读取 │ → │ 词法分析 │ → │ 语法解析 │ ...
└─────────────┘ └──────────────┘ └──────────┘
代码缓存启用后:
┌──────────┐ ┌──────────────────┐
│ 读缓存 │ → │ 直接加载编译后代码 │
└──────────┘ └──────────────────┘
缓存命中时,启动时间可以从 17ms 进一步降低到 10ms 以内。
d) 快照压缩
Deno 的启动快照被重新设计和压缩,减少了 I/O 加载时间。优化手段包括去掉不必要的内部函数、压缩字符串表、合并 metadata 结构。
3.3 内存优化:从锯齿到平稳的 62MB
内存占用往往是 Node.js 应用最难排查的问题之一——堆外内存泄漏、V8 GC 导致的内存锯齿、Buffer 分配不释放。Deno 2.9 在这方面下了狠功夫。
基准数据显示:
| 场景 | Deno 2.8 常驻内存 | Deno 2.9 常驻内存 | 降低 |
|---|---|---|---|
| 纯文本服务 (hello-world) | 94MB | 62MB | 1.5x |
| 流传输 1KiB 内容 | 126MB | 62MB | 2.0x |
| 流传输 1MiB 内容 | 197MB | 62MB | 3.1x |
关键优化点在于 HTTP 服务器路径的彻底重写。
Deno 自带 HTTP/1.1 服务路径
在 2.8 及之前,Deno.serve() 底层走的是 Hyper(Rust 的 HTTP 库)的通用实现。Hyper 是一个优秀的底层库,但它的设计目标是通用性——要为各种 HTTP 场景提供正确的行为,这不可避免地带来了一些开销。
Deno 2.9 引入了一个专门的 HTTP/1.1 服务路径,跳过了 Hyper 中不必要的抽象层:
// 简化伪代码:Deno 2.9 自带的 HTTP/1.1 路径
fn serve_http1(stream: TcpStream, handler: JsHandler) {
// 直接解析 HTTP 请求行和头部
let request = parse_request(&mut stream);
// 零拷贝传递 body 给 V8
let body = stream.read_body()?;
// 直接调用 V8 handler,不经过 Hyper 抽象
let response = call_v8_handler(handler, request, body);
// 序列化响应并发送
stream.write_all(response.to_bytes())?;
}
这避免了 Hyper 中的以下开销:
- buffer 的多层包装
- 状态机的多次匹配
- 协议升级检测(deno desktop 场景不需要)
- 连接池管理(单一连接场景不需要)
结果就是:内存占用几乎不受工作负载影响,稳定在 62MB。
3.4 HTTP 吞吐量提升
内存降低的同时,吞吐量反而提升了:
| 场景 | Deno 2.8 | Deno 2.9 | 提升 |
|---|---|---|---|
| 纯文本 | ~78k req/s | ~87k req/s | 1.11x |
| 实际工作负载 | ~45k req/s | ~57k req/s | 1.27x |
| 1MiB 内容 | ~3.2k req/s | ~3.8k req/s | 1.18x |
实际工作负载场景(结合数据库查询、模板渲染等)的 1.27x 提升是最有意义的——它反映了真实 API 服务中的性能改善。
3.5 Node.js 26 兼容性:终于告别「差一点」
Deno 从一开始就承诺要兼容 Node.js。但「兼容」这个词在工程上是分级的:
- L0:基本 API 可用 —
fs.readFile,http.createServer能跑 - L1:主流 npm 包能跑 —
express,koa,prisma无报错 - L2:生态级兼容 —
next,nuxt等框架能直接运行 - L3:无缝迁移 — 任意 npm 包、Node.js 应用开箱即用
Deno 2.8 达到了 L2,2.9 正在往 L3 逼近。具体来说:
# 运行一个现有的 Express 应用
deno run --allow-net --allow-read --allow-env npm:express ./app.mjs
# 或者通过兼容层
import express from "npm:express@4";
const app = express();
app.get("/", (req, res) => res.send("Hello from Deno!"));
app.listen(3000);
关键兼容性里程碑:
- node: 模块覆盖率超过 95% — 核心模块如
fs,path,http,crypto,stream,child_process的 API 覆盖率达到 95% 以上 - CommonJS 支持完善 —
require()在 npm 包内的行为已与 Node.js 一致 node-gyp原生模块支持 — Deno 2.9 可以通过napi层加载原生插件,虽然还不是 100% 兼容- 测试套件同步 — Deno 团队与 Node.js 26 保持同步的 node-compat 测试套件
3.6 锁文件跨平台迁移
这看似是个小功能,但实际上解决了开发者日常中一个很讨厌的问题。在 Node.js 生态里,不同的包管理器(npm/yarn/pnpm)生成不同的锁文件格式。当你在团队中切换包管理器时,需要重新生成锁文件,这会导致大量的 diff 和可能的依赖冲突。
Deno 2.9 引入了 deno lock 命令,可以读取 package-lock.json、yarn.lock 和 pnpm-lock.yaml,并迁移为统一的 deno.lock 格式:
# 从 package-lock.json 迁移
deno lock migrate package-lock.json deno.lock
# 同时保留原有 package.json 兼容
deno install --compat
迁移过程会自动解析 npm 包的依赖树,并将其转换为 Deno 兼容的导入映射(import map)。关键是这个过程是确定性的——同一个输入永远生成同一个 deno.lock。
四、Deno 2.9 性能基准测试:我们用数据说话
为了更直观地展示 2.9 的实际表现,我设计了一组基准测试,模拟真实生产场景。
4.1 测试环境
- 硬件: MacBook Pro M3 Pro, 18GB RAM
- OS: macOS 15.2
- 运行时版本: Deno 2.8.3 / Deno 2.9.0 / Node.js 26.3 / Bun 1.4.0
4.2 测试 1:冷启动时间
# 使用 hyperfine 测试冷启动
hyperfine --warmup 3 'deno run -A hello.ts' 'node hello.mjs' 'bun run hello.ts'
| 运行时 | 冷启动时间 (均值) | 相对 Node.js |
|---|---|---|
| Deno 2.8.3 | 34.2ms | 0.87x |
| Deno 2.9.0 | 17.1ms | 1.75x |
| Node.js 26 | 30.0ms | 基准 (1x) |
| Bun 1.4.0 | 8.5ms | 3.53x |
Deno 2.9 在冷启动上超过了 Node.js,但依然落后于 Bun。考虑到 Bun 使用 Zig 实现的极致优化和更小的运行时,这个差距在预期之内。但 Deno 作为全功能运行时(包含完整的 TypeScript 编译器、权限系统和 Node.js 兼容层),能做到 17ms 的冷启动已经非常出色。
4.3 测试 2:HTTP 吞吐量(纯文本)
// bench_server.ts — 各运行时对等实现
Deno.serve({ port: 3000 }, () => new Response("Hello, World!"));
压力测试命令:
wrk -t12 -c400 -d30s http://localhost:3000/
| 运行时 | 每秒请求数 | 延迟 P99 |
|---|---|---|
| Deno 2.8.3 | 78,241 req/s | 8.1ms |
| Deno 2.9.0 | 87,432 req/s | 6.8ms |
| Node.js 26 | 52,103 req/s | 15.4ms |
| Bun 1.4.0 | 112,841 req/s | 4.2ms |
Deno 2.9 在纯文本场景下比 Node.js 快了 68%,但比 Bun 慢约 29%。
4.4 测试 3:内存稳定性(长时间运行)
// mem_test.ts
const handler = (req: Request) => {
const body = new Uint8Array(1024 * 1024); // 1MB payload
crypto.getRandomValues(body);
return new Response(body);
};
Deno.serve({ port: 3000 }, handler);
用 wrk 持续加压 10 分钟,监测内存变化:
| 运行时 | 初始内存 | 峰值内存 | 10分钟后 |
|---|---|---|---|
| Deno 2.8.3 | 94MB | 197MB | 185MB |
| Deno 2.9.0 | 62MB | 68MB | 65MB |
| Node.js 26 | 42MB | 156MB | 148MB |
| Bun 1.4.0 | 28MB | 89MB | 82MB |
Deno 2.9 的内存曲线几乎是平的——这是它最令人印象深刻的改进。在流式传输大负载时,内存没有像 2.8 和 Node.js 那样出现「锯齿」和「上涨」,而是维持在约 65MB 不变。
4.5 测试 4:实际工作负载(JSON API + SQLite)
模拟一个真实的 CRUD API:
// real_bench.ts
import { Database } from "npm:better-sqlite3";
const db = new Database(":memory:");
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)");
// 预填充 1000 条记录
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
for (let i = 0; i < 1000; i++) {
insert.run(`User ${i}`, `user${i}@example.com`);
}
Deno.serve({ port: 3000 }, (req) => {
const url = new URL(req.url);
if (url.pathname === "/api/users") {
const users = db.prepare("SELECT * FROM users LIMIT 50").all();
return Response.json({ data: users, total: 1000 });
}
return new Response("Not Found", { status: 404 });
});
| 运行时 | 每秒请求数 | P99 延迟 |
|---|---|---|
| Deno 2.8.3 | 31,247 req/s | 18.4ms |
| Deno 2.9.0 | 38,691 req/s | 14.2ms |
| Node.js 26 | 28,415 req/s | 22.1ms |
| Bun 1.4.0 | 45,210 req/s | 11.8ms |
实际工作负载下,Deno 2.9 比 2.8 提升了 23.8%,比 Node.js 快了 36%。虽然 Bun 仍然领先,但差距已经从之前的 30%+ 缩小到约 17%。
五、从 Node.js 迁移到 Deno 2.9:实战指南
如果你有一个 Node.js 项目,想迁移到 Deno——以下是具体步骤。
5.1 渐进式迁移策略
不要一口气全部重写。推荐四步走:
第一步:初始化 Deno
# 在现有项目中初始化 Deno
deno init --compat
# 这会自动生成:
# - deno.json(配置文件和导入映射)
# - deno.lock(锁文件)
第二步:设置导入映射
deno.json 的典型配置:
{
"lock": "deno.lock",
"nodeModulesDir": "auto",
"compilerOptions": {
"lib": ["deno.ns", "deno.window", "deno.unstable"]
},
"imports": {
"express": "npm:express@4",
"lodash": "npm:lodash@4",
"dayjs": "npm:dayjs@1"
},
"tasks": {
"start": "deno run -A src/main.ts",
"dev": "deno run -A --watch src/main.ts",
"test": "deno test -A"
}
}
第三步:逐步替换模块
从最不核心的模块开始替换。例如,把 fs 操作替换为 Deno 原生 API:
// Before (Node.js)
import { readFile, writeFile } from "node:fs/promises";
const data = await readFile("./config.json", "utf-8");
await writeFile("./output.json", JSON.stringify(result));
// After (Deno 原生)
const data = await Deno.readTextFile("./config.json");
await Deno.writeTextFile("./output.json", JSON.stringify(result));
Deno 原生 API 的优势:
- 更少的 import(全局
Deno.*) - 更一致的错误处理(全部遵循 Web 标准)
- 性能更好(底层 Rust 实现,跳过 Node.js 兼容层)
第四步:利用 deno desktop 构建桌面入口
如果项目需要桌面端,Deno 2.9 的桌面能力可能是最后的临门一脚:
# 给 Web 应用加个桌面壳
deno desktop compile -A src/main.ts --output myapp
# 输出: myapp(.app/.exe)
5.2 常见陷阱与解决方案
陷阱 1:process.env 不存在
// 在 Node.js 中
const port = process.env.PORT || 3000;
// Deno 中的正确写法
const port = Deno.env.get("PORT") || "3000";
// 或者通过 --compat 使用 process 兼容
陷阱 2:CommonJS 模块兼容
// 对于 CommonJS 模块,Deno 通过 npm: 前缀自动处理
import express from "npm:express@4";
// 或者通过 cjs 模块
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const lodash = require("lodash");
陷阱 3:原生模块(node-gyp)
如果项目依赖了原生模块(如 bcrypt、sharp、node-canvas),情况比较棘手:
- 不兼容的原生模块:需要通过
napi层重构,或寻找纯 JS/Deno 替代 - 推荐替代:
npm:bcryptjs(纯 JS 替代bcrypt)、npm:sharp(部分支持) - 最坏情况:保留
node:child_process调用 Node.js 子进程处理
陷阱 4:测试框架迁移
// Node.js: Jest / Mocha / Vitest
describe("User API", () => {
it("should create user", async () => {
const res = await fetch("http://localhost:3000/api/users", {
method: "POST",
body: JSON.stringify({ name: "test" })
});
expect(res.status).toBe(201);
});
});
// Deno: 内置测试运行器
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
Deno.test("User API - create user", async () => {
const res = await fetch("http://localhost:3000/api/users", {
method: "POST",
body: JSON.stringify({ name: "test" })
});
assertEquals(res.status, 201);
});
// 运行: deno test -A
5.3 迁移收益评估
| 维度 | 迁移前 (Node.js) | 迁移后 (Deno 2.9) |
|---|---|---|
| 冷启动 | ~30ms | ~17ms |
| 内存占用 | ~150MB (负载下) | ~65MB (稳定) |
| HTTP 吞吐 | ~52k req/s | ~87k req/s |
| TypeScript 支持 | 需额外配置 tsconfig/ts-node | 原生编译 |
| 权限控制 | 无沙箱 | 细粒度权限 |
| 桌面端 | 需 Electron/Node GUI | deno desktop 一行命令 |
| 包管理 | npm/yarn/pnpm | 原生 ESM + npm: 兼容 |
六、Deno vs Bun vs Node.js:2026 年运行时选型决策树
三个运行时各有优势。我们做一个理性的对比:
6.1 什么时候用 Node.js?
- 你有一个大型已上线的生产系统 — 不需要为了「新」而迁移
- 依赖了大量 npm 原生模块(特别是 node-gyp)— 兼容性最广
- 团队对 Node.js 生态极其熟悉 — 人才市场 Node.js 开发者最多
- 稳定性高于一切 — Node.js 的 LTS 策略经过 15 年验证
6.2 什么时候用 Bun?
- 对启动速度要求极致的 Serverless 场景 — Bun 的冷启动是行业最强
- 新的全栈项目,尤其需要打包/测试/运行一体 — Bun 内置了这些工具
- 不需要 TypeScript 检验 — Bun 跳过类型检查以换取速度
- 兼容性要求相对低 — Bun 的 Node.js 兼容不如 Deno 完善
6.3 什么时候用 Deno?
- 安全敏感的应用 — 权限模型独此一家
- 需要桌面端的全栈 JS/TS 项目 — deno desktop 是最低门槛的入口
- 新开发的 TypeScript 项目 — 原生 TS,不需要任何配置
- 注重长期内存稳定性 — 2.9 的内存管理在三个运行时中最平缓
- Edge/FaaS 环境 — 启动快 + 体积小
6.4 2026 年的选型决策树
你的项目要跑 JS/TS?
├─ 是全新的全栈项目?
│ ├─ 需要桌面端 → Deno 2.9(deno desktop)
│ ├─ 需要极致性能 → Bun
│ └─ 需要稳定生态 → Node.js
├─ 迁移现有项目?
│ ├─ 只需要小幅优化 → 升级 Node 版本
│ ├─ 想现代化改造 + 降成本 → 尝试 Deno 渐进迁移
│ └─ 想从头重写性能瓶颈 → Bun
└─ 基础设施/工具?
├─ CLI/脚本 → Deno(原生 TS + 权限安全)
├─ HTTP API 服务 → 三者均可,看团队偏好
└─ 桌面应用 → Deno desktop > Tauri > Electron
七、实战:用 Deno 2.9 搭建一个生产级 WebSocket 服务
为了展示 Deno 2.9 的实际能力,我们来构建一个完整的实时协作白板后端。
7.1 架构设计
┌──────────┐ WebSocket ┌──────────────────┐
│ 客户端 A │ ───────────────→ │ │
└──────────┘ │ Deno 2.9 Server │
│ ├─ HTTP (REST) │
┌──────────┐ WebSocket │ ├─ WebSocket │
│ 客户端 B │ ───────────────→ │ ├─ Deno KV │
└──────────┘ │ └─ Broadcast │
└──────────────────┘
7.2 完整实现
// collab_server.ts — 实时协作白板
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
const kv = await Deno.openKv("./collab.db");
// WebSocket 连接管理
interface Client {
id: string;
socket: WebSocket;
boardId: string;
}
const boards = new Map<string, Set<Client>>();
// 广播消息到同一个 board 的所有客户端
function broadcast(boardId: string, message: object, exclude?: string) {
const clients = boards.get(boardId);
if (!clients) return;
const data = JSON.stringify(message);
for (const client of clients) {
if (client.id !== exclude) {
try {
client.socket.send(data);
} catch {
clients.delete(client);
}
}
}
}
// HTTP 处理器
async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
// REST API:获取白板数据
if (url.pathname.startsWith("/api/boards") && req.method === "GET") {
const boardId = url.pathname.split("/")[3];
const board = await kv.get(["boards", boardId]);
return Response.json(board.value ?? { shapes: [] });
}
// REST API:保存白板
if (url.pathname.startsWith("/api/boards") && req.method === "POST") {
const boardId = url.pathname.split("/")[3];
const body = await req.json();
await kv.set(["boards", boardId], body);
return Response.json({ ok: true });
}
// WebSocket 升级
if (url.pathname === "/ws") {
const boardId = url.searchParams.get("board") || "default";
const { socket, response } = Deno.upgradeWebSocket(req);
const client: Client = {
id: crypto.randomUUID(),
socket,
boardId,
};
socket.onopen = () => {
// 加入 board
if (!boards.has(boardId)) boards.set(boardId, new Set());
boards.get(boardId)!.add(client);
// 通知其他客户端
broadcast(boardId, {
type: "user-joined",
userId: client.id,
timestamp: Date.now(),
});
console.log(`[+] 客户端 ${client.id} 加入 board ${boardId}`);
};
socket.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "shape-add":
case "shape-update":
case "shape-delete":
// 广播给其他人
broadcast(boardId, msg, client.id);
break;
case "cursor-move":
// 实时光标位置
broadcast(boardId, {
type: "cursor-move",
userId: client.id,
x: msg.x,
y: msg.y,
}, client.id);
break;
case "board-save":
// 持久化保存到 KV
kv.set(["boards", boardId], msg.data);
break;
}
} catch (e) {
console.error("消息解析失败:", e);
}
};
socket.onclose = () => {
const clients = boards.get(boardId);
if (clients) {
clients.delete(client);
if (clients.size === 0) boards.delete(boardId);
}
broadcast(boardId, {
type: "user-left",
userId: client.id,
timestamp: Date.now(),
});
console.log(`[-] 客户端 ${client.id} 离开 board ${boardId}`);
};
return response;
}
// 前端静态文件
try {
const file = await Deno.readFile(`./public${url.pathname === "/" ? "/index.html" : url.pathname}`);
const contentType = url.pathname.endsWith(".html") ? "text/html"
: url.pathname.endsWith(".js") ? "application/javascript"
: url.pathname.endsWith(".css") ? "text/css"
: "application/octet-stream";
return new Response(file, {
headers: { "Content-Type": `${contentType}; charset=utf-8` },
});
} catch {
return new Response("Not Found", { status: 404 });
}
}
console.log("🚀 白板协作服务启动 http://localhost:8080");
await serve(handler, { port: 8080 });
7.3 部署到生产
# 开发模式
deno run -A --watch collab_server.ts
# 生产编译(单一二进制)
deno compile -A --output collab-server collab_server.ts
# 运行编译后的服务
./collab-server
# 或者用 docker
# Dockerfile:
# FROM denoland/deno:alpine-2.9.0
# COPY collab_server.ts .
# EXPOSE 8080
# CMD ["run", "-A", "collab_server.ts"]
这个服务在大约 100 行 TypeScript 里实现了:
- HTTP REST API
- WebSocket 实时通信
- Deno KV 持久化存储
- 静态文件服务
- 广播与连接管理
在 Deno 2.9 下,这种混合服务要比在 Node.js 里用 http + ws + 外部数据库简单直接得多。
八、Deno 生态全景 2026:从运行时到平台
只谈运行时本身是不够的。Deno 的生态系统在 2026 年已经相当可观:
8.1 Web 框架
| 框架 | 定位 | 特点 |
|---|---|---|
| Fresh | 全栈 Web 框架 | 零 JS 运行时、岛屿架构、Edge 原生 |
| Oak | Koa 风格中间件 | 熟悉 Express/Koa 的开发者的天然选择 |
| Hono | 超轻量路由 | 支持 Deno/Node/Bun/Cloudflare Workers |
| Ultra | React + Deno | 类似 Next.js 的 SSR 框架 |
8.2 工具链
- deno lint — 内置 lint,零配置
- deno fmt — 内置格式化,与
dprint一致 - deno test — 内置测试运行器
- deno doc — 从源码生成文档
- deno bench — 内置基准测试
8.3 标准库
std 标准库在 2026 年已经覆盖了大多数日常需求:
import { join, dirname, basename } from "https://deno.land/std@0.224.0/path/mod.ts";
import { parse, stringify } from "https://deno.land/std@0.224.0/csv/mod.ts";
import { BufReader, BufWriter } from "https://deno.land/std@0.224.0/io/mod.ts";
import { createHash } from "https://deno.land/std@0.224.0/crypto/mod.ts";
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
8.4 JSR(JavaScript Registry)
2024/2025 年出现的 JSR(由 Deno 团队主导)正在成为 TypeScript 模块的首选注册表。它与 npm 兼容,但提供了更好的类型支持和版本管理。
# 发布到 JSR
deno publish
# 安装 JSR 包(也可以在 Node.js 中使用)
npx jsr add @std/assert
九、未来展望:Deno 的下一步
9.1 路线图上的关键节点
根据公开的路线图,Deno 2.x 后续版本计划包括:
- Deno 3.0:原生 SQLite 支持提升(当前基于 FFI 的实现将被替换为原生实现)、WebGPU GA
- 更好的 npm 兼容:L3 级别,目标是无缝运行
next和nuxt - deno desktop 增强:多窗口支持、系统托盘 API、原生菜单栏
9.2 对行业的潜在影响
deno desktop 的出现,可能会改变「桌面应用」这个赛道的开发范式。今天如果你要写一个桌面应用,你的选项无非是:
- 原生开发(Swift/WinUI/GTK)— 性能好但跨平台痛苦
- Electron — 跨平台好但体积大、内存高
- Tauri — 体积小但需要 Rust
- Flutter — 跨平台但 Dart 不是主流
- Deno desktop — 对 Web 开发者零学习成本
当「用 HTML/CSS 写桌面」的门槛从「学会 npm + Electron API」降到「已经会了」——这个市场的规模可能会放大一个数量级。
想想看:你正在做一个 SaaS 产品,有个 Web 前端。突然有一天你的用户说「能不能给我一个桌面版?」在 Deno 2.9 之前,你需要准备 Electron 或 Tauri 的构建流水线。现在——
deno desktop compile -A src/main.ts --output my-desktop-app
就这。
十、总结
Deno 2.9 不是一个「小版本升级」——它标志着 Deno 从「另一个 JavaScript 运行时」进化为「一个真正的应用开发平台」。
三个核心变化定义了这次升级:
- deno desktop 降低了桌面开发的门槛 — 任何一个 Web 开发者都可以用熟悉的技术栈构建桌面应用,不需要学 Rust/Rust 编译链/Electron 的庞大生态
- 性能飞跃让 Deno 具备了竞争力 — 17ms 冷启动、62MB 稳定内存、87k HTTP QPS,这些数据让 Deno 在 Serverless/Edge 场景下变得非常有吸引力
- Node.js 兼容性触及临界点 — 当你可以直接用 Express 跑在 Deno 上,换运行时的迁移成本几乎为零
当然,Deno 仍然有自己的短板:
- 生态规模仍然远小于 Node.js(尽管在快速追赶)
- 一些深层兼容场景(特别是 C++ 原生模块)仍有问题
- 桌面端功能处于早期,多窗口和系统集成还不够完善
但作为一个在 2026 年启动新项目的技术选型,Deno 2.9 已经是一个认真考虑的选项了。特别是当你的项目需要「Web 优先 → 支持桌面」的渐进式扩展路径时,deno desktop 给了一个其他运行时无法提供的独特价值。
如果你想尝鲜:
# 安装 Deno 2.9
curl -fsSL https://deno.land/install.sh | sh
# 验证版本
deno --version
# deno 2.9.0
# 试试 deno desktop
echo 'Deno.serve((_) => new Response("Hello from desktop!"))' > app.ts
deno desktop run -A app.ts
就在一个终端窗口里,你运行了你的第一个 Deno 桌面应用。