前两天刷掘金热榜看到 Electrobun 这个名字,第一反应是——又一个 Electron 替代品?Tauri 不是已经卷过一轮了吗?
但是当我看到打包体积 12MB 的时候,还是没忍住试了一下。结果一个下午就撸出了一个能用的 AI 聊天桌面助手,打包完一看体积,确实有点离谱。
先说结论
| 对比项 | Electron | Tauri | Electrobun |
|---|---|---|---|
| 运行时 | Node.js + Chromium | Rust + 系统 WebView | Bun + 系统 WebView |
| 开发语言 | JS/TS | Rust + JS/TS | 纯 TypeScript |
| Hello World 包体积 | ~270MB | ~8MB | ~12MB |
| 冷启动速度 | 慢 | 快 | 很快 |
| 学习成本 | 低 | 高(要会 Rust) | 低 |
| 生态成熟度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐(刚 v1) |
Tauri 体积更小,但你得写 Rust。Electrobun 的卖点就是:纯 TypeScript 全栈,不用学新语言,体积还能压到 12MB 级别。
为什么不用 Electron?
不是黑 Electron,我之前的几个小工具都是 Electron 写的。但问题真的很实际:
- 一个 Hello World 就 270MB,用户下载要等半天
- 每个 Electron 应用都自带一个 Chromium,开 3 个 Electron 应用等于开了 3 个 Chrome
- 内存占用,随便一个小工具就吃 200MB+ 内存
Tauri 解决了体积和性能问题,但代价是你得写 Rust。对于纯前端来说,这个门槛确实不低。
Electrobun 的思路是:用 Bun 替代 Node.js 做主进程(Bun 本身就比 Node 快),渲染层用操作系统自带的 WebView(macOS 用 WebKit,Windows 用 Edge WebView2),不捆绑浏览器引擎。
上手:5 分钟跑起来
前置条件:装好 Bun(没装的话 curl -fsSL https://bun.sh/install | bash)。
# 创建项目
bunx electrobun init my-ai-chat
cd my-ai-chat
# 装依赖
bun install
# 跑起来
bun run dev
跑完你会看到一个原生窗口弹出来,里面是一个简单的欢迎页面。整个过程不到 1 分钟。
项目结构长这样:
my-ai-chat/
├── src/
│ ├── main.ts # 主进程(Bun 环境)
│ └── renderer/
│ ├── index.html # 页面
│ ├── style.css # 样式
│ └── script.ts # 前端逻辑
├── electrobun.config.ts # 构建配置
└── package.json
和 Electron 的结构很像,main.ts 对应 Electron 的 main.js,renderer/ 对应渲染进程。
改造成 AI 聊天助手
我的目标:做一个桌面版的 AI 聊天工具,支持多模型切换(GPT-4o、Claude、DeepSeek 等),流式输出。
主进程:创建窗口 + IPC
// src/main.ts
import { BrowserWindow } from "electrobun/bun";
const win = new BrowserWindow({
title: "AI Chat Desktop",
width: 800,
height: 600,
url: "electrobun://renderer/index.html",
});
// 监听渲染进程发来的消息
win.webview.onMessage("chat-request", async (data) => {
const { model, messages } = data;
try {
// 调用 AI API,流式返回
const response = await fetch("https://api.ofox.ai/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OFOX_API_KEY}`,
},
body: JSON.stringify({
model: model,
messages: messages,
stream: true,
}),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n").filter((line) => line.startsWith("data:"));
for (const line of lines) {
const json = line.slice(5).trim();
if (json === "[DONE]") continue;
try {
const parsed = JSON.parse(json);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
// 实时推送到渲染进程
win.webview.sendMessage("chat-stream", { content });
}
} catch {}
}
}
win.webview.sendMessage("chat-done", {});
} catch (err) {
win.webview.sendMessage("chat-error", { error: String(err) });
}
});
这里有个细节:Electrobun 的 IPC 通信用的是 onMessage / sendMessage,比 Electron 的 ipcMain / ipcRenderer 简洁不少。不需要单独写 preload 脚本。
渲染进程:聊天界面
<!-- src/renderer/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>AI Chat</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="app">
<div class="header">
<select id="model-select">
<option value="gpt-4o">GPT-4o</option>
<option value="claude-sonnet-4-20250514">Claude Sonnet 4</option>
<option value="deepseek-chat">DeepSeek V3</option>
<option value="qwen-plus">Qwen Plus</option>
</select>
</div>
<div id="messages" class="messages"></div>
<div class="input-area">
<textarea id="input" placeholder="输入消息..." rows="3"></textarea>
<button id="send-btn">发送</button>
</div>
</div>
<script src="./script.ts"></script>
</body>
</html>
// src/renderer/script.ts
import { webview } from "electrobun/webview";
const messagesDiv = document.getElementById("messages")!;
const input = document.getElementById("input") as HTMLTextAreaElement;
const sendBtn = document.getElementById("send-btn")!;
const modelSelect = document.getElementById("model-select") as HTMLSelectElement;
let chatHistory: Array<{ role: string; content: string }> = [];
let currentAssistantMsg: HTMLDivElement | null = null;
function addMessage(role: string, content: string) {
const div = document.createElement("div");
div.className = `message ${role}`;
div.textContent = content;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
return div;
}
sendBtn.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
// 显示用户消息
addMessage("user", text);
chatHistory.push({ role: "user", content: text });
// 创建助手消息占位
currentAssistantMsg = addMessage("assistant", "") as HTMLDivElement;
// 发送到主进程
webview.sendMessage("chat-request", {
model: modelSelect.value,
messages: chatHistory,
});
input.value = "";
sendBtn.disabled = true;
});
// 接收流式响应
webview.onMessage("chat-stream", (data) => {
if (currentAssistantMsg) {
currentAssistantMsg.textContent += data.content;
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
});
webview.onMessage("chat-done", () => {
if (currentAssistantMsg) {
chatHistory.push({
role: "assistant",
content: currentAssistantMsg.textContent || "",
});
}
currentAssistantMsg = null;
sendBtn.disabled = false;
});
// Ctrl+Enter 发送
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
sendBtn.click();
}
});
样式(简洁暗色主题)
/* src/renderer/style.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #1a1a2e;
color: #eee;
height: 100vh;
}
#app {
display: flex;
flex-direction: column;
height: 100%;
}
.header {
padding: 12px 16px;
background: #16213e;
border-bottom: 1px solid #333;
}
.header select {
background: #0f3460;
color: #eee;
border: 1px solid #444;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.message {
margin-bottom: 12px;
padding: 10px 14px;
border-radius: 10px;
max-width: 80%;
line-height: 1.6;
white-space: pre-wrap;
}
.message.user {
background: #0f3460;
margin-left: auto;
}
.message.assistant {
background: #1a1a3e;
border: 1px solid #333;
}
.input-area {
display: flex;
gap: 8px;
padding: 12px 16px;
background: #16213e;
border-top: 1px solid #333;
}
.input-area textarea {
flex: 1;
background: #0f3460;
color: #eee;
border: 1px solid #444;
border-radius: 8px;
padding: 10px;
font-size: 14px;
resize: none;
}
.input-area button {
background: #e94560;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.input-area button:hover { background: #c81e45; }
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
打包体验
bun run build
打包完成后:
dist/
└── AI Chat Desktop.app # macOS
└── (总大小: ~64MB)
等等,说好的 12MB 呢?
这里要解释一下:12MB 是 Electrobun 框架本身的开销,加上你的业务代码和依赖。我的项目因为没有额外的 npm 包(AI 调用用的是原生 fetch),实际打包大约 64MB,主要是 Bun runtime 占了大头。
作为对比,同样功能的 Electron 版本打包后 310MB。差了将近 5 倍。
而且 Electrobun 有个杀手锏:差分更新。它内置了增量更新机制,版本迭代时只推送差异补丁,补丁大小可以小到 14KB。Electron 每次更新基本要重新下载整个 Chromium。
踩坑记录
1. Bun 版本兼容
Electrobun v1 要求 Bun >= 1.2。我一开始用的 1.1.x,bunx electrobun init 直接报了一堆类型错误。升级 Bun 后就好了:
bun upgrade
2. 环境变量加载
Bun 主进程不会自动读 .env 文件。需要手动加载:
// main.ts 顶部
import { $ } from "bun";
// 或者直接在 electrobun.config.ts 里配 env
我最后的方案是在 electrobun.config.ts 的 env 字段里写死(开发时),打包时从系统环境变量读取。
3. WebView 兼容性
macOS 上用的是 WebKit,不是 Chromium。这意味着一些 Chrome 特有的 API 不能用。我一开始用了 structuredClone 在 IPC 里传数据,结果在某些 macOS 版本上挂了。改成 JSON.parse(JSON.stringify(...)) 就没问题了。
4. 流式响应的坑
Bun 的 fetch 对 SSE 流式响应的支持和 Node.js 有点不一样。response.body 返回的是 ReadableStream,需要用 getReader() 来读,不能直接 for await...of。这个搞了我半小时。
值不值得用?
说实话,Electrobun 目前还是 v1 早期阶段,生态和 Electron 没法比。但如果你的场景是:
- 轻量级工具类应用(不需要复杂原生功能)
- 对包体积敏感(给客户分发不想让人等半天)
- 团队全是前端,不想碰 Rust
- 想尝鲜 Bun 生态
那完全值得试试。
我这个 AI 聊天桌面助手的完整流程:从 bunx electrobun init 到打包出可用的 .app,大概 3 小时(包括踩坑时间)。体验下来比第一次用 Electron 顺畅不少,至少不用折腾 webpack 配置和 preload 脚本。
API 层我用的是兼容 OpenAI 协议的聚合接口,改个 base_url 就能切不同模型,省得每个模型单独对接 SDK。如果你也想做类似的多模型桌面工具,这个思路可以参考。
完整代码我后续整理后会放 GitHub,有兴趣的可以先 mark 一下。