乐于分享
好东西不私藏

告别几百MB的安装包!这个Rust框架让桌面应用轻如鸿毛

告别几百MB的安装包!这个Rust框架让桌面应用轻如鸿毛

大家好,我是阿浪

不知道大家有没有遇到过这种情况:明明只是想写个给团队内部用的简单小工具,结果用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"
然后写了一个main.rs
use electrico::prelude::*;fn main() -> Result<(), ElectricoError> {    let app = Application::builder()        .window(Window::builder()            .title("我的第一个Electrico应用")            .inner_size(800600)            .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<StringString{    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函数,攻击面小了很多。

适合什么样的项目?

我觉得这三类场景特别适合:

  1. 团队内部工具 —— 不求花哨,只求实用,体积小方便分发。

  2. 个人小工具 —— 比如剪切板增强、Markdown预览、定时提醒等。

  3. 原型快速验证 —— 想用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"
main.rs
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-width500pxmargin2rem auto; padding0 1rem; }        .todo-item { display: flex; align-items: center; gap0.75rempadding0.5rem 0border-bottom1px solid #eee; }        .todo-item.done span { text-decoration: line-through; color#999; }        .add-area { display: flex; gap0.5remmargin-bottom1.5rem; }        .add-area input { flex1padding0.5remborder1px solid #cccborder-radius6px; }        .add-area button { padding0.5rem 1.2rembackground#007affcolor: white; border: none; border-radius6pxcursor: pointer; }        .del-btn { color#ff3b30background: none; border: none; cursor: pointer; font-size1.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, donefalse });            input.value = '';            save().then(render);        }        function toggle(id) {            const item = todos.find(t => t.id === id);            if (item) { item.done = !item.donesave().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,而是给那些不需要那么重的场景,提供一个更优雅的出路。如果你正在为下一个桌面工具选型,不妨花半个小时把它跑起来试试。反正代码就那么几行,成不成都不会亏。

至少我试完之后,再也不想为了一个内部小工具去拖一个两百兆的安装包了。

如果觉得阿浪的文章写得还行,麻烦动动小手点个关注,点个赞支持阿浪一下,我会持续更新更多硬核干货,谢谢大家!