大家好,我是阿浪
不知道大家有没有遇到过这种情况:明明只是想写个给团队内部用的简单小工具,结果用Electron打包出来,安装包直奔200MB,用户下载的时候骂骂咧咧,启动的时候内存占用直接飙到500MB+,同事还以为你写了个3A大作。
我经历过。那时候我做了个简单的待办事项管理面板,功能也就增删改查加个本地存储,结果发给测试同事,对方第一反应是:“你这东西怎么比我的IDE还大?”搞得我挺尴尬。
后来我开始琢磨,有没有一种方案,既能用Web技术写界面,又能把体积和内存消耗降下来?直到我遇到了Electrico。
它到底是什么?
Electrico是一个基于Rust的桌面应用框架,你可以把它理解为“Electron的瘦身版”。但它不是简单地在Electron外面套壳,而是彻底换了一套底层实现。

它不用Node.js,不用Chromium,而是直接用Rust调用系统自带的WebView组件。Windows上是Edge WebView2,macOS上是WKWebView,Linux上是WebKitGTK。这样一来,渲染引擎是系统自带的,不用每次打包都塞一个几百MB的浏览器内核进去。
打个比方:Electron是你在房子里又盖了个小房间,而Electrico是直接借用了客厅的电视屏幕。 该有的显示功能一样不少,但省下了整面墙的砖头和水泥。
我第一次跑起来的感受
按照官方文档的指引,我建了一个新项目,Cargo.toml里加上依赖:
[package]name = "my-electrico-app"version = "0.1.0"edition = "2021"[dependencies]electrico = "0.1"
use electrico::prelude::*;fn main() -> Result<(), ElectricoError> {let app = Application::builder().window(Window::builder().title("我的第一个Electrico应用").inner_size(800, 600).build()?).build()?;app.run()?;Ok(())}
编译、运行,一个空窗口弹出来了,标题是我写的。整个编译过程很快,生成的二进制文件不到10MB。我当时就愣了一下——以前用Electron,光node_modules就比我整个项目还大。
当然,光一个空窗口没意思,我得把界面塞进去。Electrico支持加载本地HTML文件,也可以直接嵌入HTML字符串。我写了个简单的前端页面,用app.load_html()加载:
let html = r#"<!DOCTYPE html><html><head><metacharset="UTF-8"></head><bodystyle="font-family: sans-serif; padding: 2rem;"><h1> 你好,Electrico</h1><p>当前时间: <spanid="time"></span></p><buttononclick="document.getElementById('time').innerText = new Date().toLocaleString()">刷新时间</button></body></html>"#;// 在窗口创建后加载app.window().load_html(html)?;
刷新时间、点击交互,一切都跟浏览器里没区别。但注意,这里没有Chrome DevTools那么庞大的调试工具,不过你可以用系统WebView自带的检查器(比如Windows上右键->检查元素),够用了。
它和Electron到底差在哪儿?
我用了一个周末,把一个之前用Electron写的笔记小工具迁移到了Electrico。说说我的真实体验:
体积上的差距是肉眼可见的。 原先的Electron版本打包后是187MB,迁移后只有11.3MB。传给同事的时候,微信直接秒传,不用等半天。
内存占用也有明显改善。 同一个应用,Electron启动后占450MB左右,Electrico只有90MB出头。虽然不能说“极低”,但对于日常工具来说,已经舒服太多了。
但是,API覆盖度确实还有差距。 Electrico目前实现了窗口管理、文件系统读写、剪贴板、托盘图标等常用功能,但像ipcMain/ipcRenderer这种复杂的进程通信模型,它简化了很多。如果你之前的Electron项目重度依赖大量的主进程-渲染进程消息往来,迁移起来要改不少代码。
比如读写本地文件,Electrico的做法更直接:
// 在Rust侧暴露一个函数给前端调用use electrico::prelude::*;use std::fs;#[electrico::function]fnread_note(path: String) -> Result<String, String> {fs::read_to_string(path).map_err(|e| e.to_string())}// 前端通过 window.electrico.invoke('read_note', './note.txt') 调用
这种设计比Electron的IPC要轻量不少,前端直接调用Rust函数,返回Promise,异步处理。我个人更喜欢这种风格,少了一层消息序列化的折腾。
我自己的几点思考
先说不好的地方:生态确实还嫩。 想找个现成的UI组件库?抱歉,你得自己写,或者用纯前端的库(比如Vue/React)打包后丢进去,因为Electrico只管承载HTML,不管你的前端框架。这意味着你需要自己搞定构建工具(Vite/Webpack)来打包前端资源。
还有就是调试体验不如Electron成熟。Electron有完整的Chrome DevTools,而Electrico只能靠系统WebView的调试工具,功能没那么全,有时候报错信息也不够友好。
但好的地方也很明显:它让我重新思考“我真的需要Chromium吗?” 大多数内部工具、配置面板、数据看板,根本用不到Chrome那些高级特性(比如WebGL、复杂的CSS动画),一个系统WebView完全够用。我们之前动不动就上Electron,其实是“路径依赖”在作祟。
另外,Rust带来的安全性也值得一提。Electron的Node集成经常被诟病有安全风险,而Electrico默认关闭了任意代码执行通道,前端只能调用你明确暴露的Rust函数,攻击面小了很多。
适合什么样的项目?
我觉得这三类场景特别适合:
团队内部工具 —— 不求花哨,只求实用,体积小方便分发。
个人小工具 —— 比如剪切板增强、Markdown预览、定时提醒等。
原型快速验证 —— 想用Web技术写桌面原型,又不想忍受Electron的启动速度。
如果你正在做一个大型商业应用,需要深度集成第三方原生库、复杂的多窗口管理,那Electron可能还是更稳妥。但对于“小而美”的场景,Electrico给了我一个非常舒服的选择。
一个完整的示例
最后,我贴一个自己写的完整示例,是一个简单的“待办事项”桌面工具,数据存在本地JSON文件里。
Cargo.toml
[package]name = "todo-electrico"version = "0.1.0"edition = "2021"[dependencies]electrico = "0.1"serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"
use electrico::prelude::*;use serde::{Deserialize, Serialize};use std::fs;use std::path::PathBuf;#[derive(Serialize, Deserialize)]struct TodoItem {id: u32,text: String,done: bool,}type TodoList = Vec<TodoItem>;const DATA_FILE: &str = "todos.json";fn get_data_path() -> PathBuf {let dir = directories::ProjectDirs::from("com", "example", "todo-app").unwrap().data_dir().to_path_buf();std::fs::create_dir_all(&dir).unwrap();dir.join(DATA_FILE)}// 暴露给前端的Rust函数#[electrico::function]fn load_todos() -> Result<String, String> {let path = get_data_path();if !path.exists() {return Ok("[]".to_string());}let content = fs::read_to_string(path).map_err(|e| e.to_string())?;Ok(content)}#[electrico::function]fn save_todos(json: String) -> Result<(), String> {let path = get_data_path();fs::write(path, json).map_err(|e| e.to_string())?;Ok(())}fn main() -> Result<(), ElectricoError> {let html = r#"<!DOCTYPE html><html><head><metacharset="UTF-8"><title>待办清单</title><style>body { font-family: -apple-system, sans-serif; max-width: 500px; margin: 2rem auto; padding: 0 1rem; }.todo-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid #eee; }.todo-item.done span { text-decoration: line-through; color: #999; }.add-area { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }.add-area input { flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 6px; }.add-area button { padding: 0.5rem 1.2rem; background: #007aff; color: white; border: none; border-radius: 6px; cursor: pointer; }.del-btn { color: #ff3b30; background: none; border: none; cursor: pointer; font-size: 1.1rem; }</style></head><body><h2>📋 我的待办</h2><divclass="add-area"><inputid="newInput"placeholder="输入新任务..." /><buttononclick="addTodo()">添加</button></div><divid="list"></div><script>let todos = [];// 调用Rust函数async function load() {const json = await window.electrico.invoke('load_todos');todos = JSON.parse(json || '[]');render();}async function save() {await window.electrico.invoke('save_todos', JSON.stringify(todos));}function render() {const container = document.getElementById('list');container.innerHTML = todos.map(t => `<div class="todo-item ${t.done ? 'done' : ''}"><input type="checkbox" ${t.done ? 'checked' : ''}onchange="toggle(${t.id})" /><span>${t.text}</span><button class="del-btn" onclick="remove(${t.id})">✕</button></div>`).join('');}function addTodo() {const input = document.getElementById('newInput');const text = input.value.trim();if (!text) return;const id = Date.now();todos.push({ id, text, done: false });input.value = '';save().then(render);}function toggle(id) {const item = todos.find(t => t.id === id);if (item) { item.done = !item.done; save().then(render); }}function remove(id) {todos = todos.filter(t => t.id !== id);save().then(render);}load();</script></body></html>"#;let app = Application::builder().window(Window::builder().title("待办清单 · Electrico").inner_size(540, 700).build()?).build()?;app.window().load_html(html)?;app.run()?;Ok(())}
最后说几句
工具链的演进总是这样:先是百花齐放,然后大而全的框架成为主流,再后来有人开始做“减法”。Electrico就是那个做减法的角色。
它不是要取代Electron,而是给那些不需要那么重的场景,提供一个更优雅的出路。如果你正在为下一个桌面工具选型,不妨花半个小时把它跑起来试试。反正代码就那么几行,成不成都不会亏。
至少我试完之后,再也不想为了一个内部小工具去拖一个两百兆的安装包了。
夜雨聆风