欢迎光临
我们一直在努力

Electron 跑在鸿蒙 PC 上,窗口通信居然比 Windows 慢 8 倍?我差点把架构推倒重来

Electron 跑在鸿蒙 PC 上,窗口通信居然比 Windows 慢 8 倍?我差点把架构推倒重来

等等,先把 devtools 打开——我盯 profiler 看了快半小时了,实在忍不住骂一句。

事情是这样的。我那个 Electron 跨端 App(就是雷达鸭的桌面版,从微信小程序迁过去的)最近在鸿蒙 PC 上跑,窗口间通信总感觉慢半拍。Windows 上一个"点击卡片 → 主进程查数据 → 回渲染"的链路 4ms 就完事,到了鸿蒙 PC 我用同一份代码测,居然 33ms。33ms 啊,手指按下去到屏幕有反应,肉眼能感觉到那一丝迟钝。我一开始以为是鸿蒙 PC 的 ARM 架构问题,毕竟 x86 换 ARM 谁不得重新调一遍,于是我花了三天时间调 V8 snapshot、关掉 sandbox、把 GPU 进程和主进程合到一起……结果还是 33ms。

你猜怎么着,问题根本不在架构上。

第一个反常识:慢的根源不是 ARM

我先给个对比数据,避免你怀疑我编的。测试方法是开两个 BrowserWindow,A 窗口发消息给 B 窗口,B 收到后回执,A 算总往返时间。鸿蒙 PC 是 MateBook X Pro 2024 鸿蒙版,Windows 对比机是同型号的 Windows 11 版(同一台机器双系统)。

// ipc-benchmark.ets — 主进程
import { ipcMain, BrowserWindow } from 'electron';

ipcMain.handle('ping', async (event, payload) => {
// 模拟主进程简单处理:读一个本地 JSON
const start = Date.now();
const data = require('./mock-data.json'); // 12KB
return { echo: payload, size: data.length, processTime: Date.now() start };
});

// 在 A 窗口的 renderer 里循环调用
async function benchmark(rounds = 100) {
const samples: number[] = [];
for (let i = 0; i < rounds; i++) {
const t0 = performance.now();
await window.electronAPI.invoke('ping', i);
samples.push(performance.now() t0);
}
samples.sort((a, b) => a b);
return {
p50: samples[50],
p95: samples[95],
p99: samples[99]
};
}

跑三轮,每轮 100 次:

平台p50p95p99
Windows 11 (x86) 3.8ms 5.2ms 7.1ms
鸿蒙 PC (ARM) 28.4ms 33.1ms 41.7ms

p50 慢了 7.5 倍,p99 慢 5.8 倍。我盯着这个表格的时候,第一反应是"ARM 性能拉胯"。然后我跑了一组纯 CPU 密集测试——把 ping 处理器换成 1ms 的 setTimeout 模拟业务逻辑,鸿蒙反而只比 Windows 慢 30%,完全在一个数量级内。这说明问题不在 CPU 调度,而在 IPC 通道本身。

第二个反常识:不是鸿蒙慢,是 invoke 慢

我开始怀疑是 ipcMain.handle 的实现层有性能损耗。翻 Electron 源码的过程中,我看到 invoke 内部走的是 MessageChannelMain——这是 Electron 18 之后给 invoke/handle 配套设计的消息中转机制。

简单说,invoke 干了这几件事:

  • renderer 端把消息 serialize
  • 投送到主进程的 MessageChannelMain 端口
  • 主进程 handler 处理
  • 结果再走一遍 MessageChannelMain 回到 renderer
  • 在 Windows 上这个通道几乎零成本,因为 chromium IPC 优化得已经很好了。但鸿蒙 PC 的 chromium 是定制的(HarmonyOS Embedded Chromium),它的 IPC 沙箱策略和 Windows 不一样——跨进程消息必须经过一个叫 mojo::core::ScopedIPCSupport 的桥接层,每条消息多走一次用户态到内核态的切换。

    我靠这个发现差点想骂 Electron 团队——但后来冷静想想,问题不在 Electron,问题在我没看官方文档对 MessagePort 的描述。

    真正的银弹:MessagePort 直连

    MessagePort 是 Web 标准里的消息通道 API,Electron 早早就支持了。它和 invoke/handle 的关键区别:端口是预创建的,消息不走中转桥。

    // ipc-with-messageport.ets — 主进程
    import { MessageChannelMain, BrowserWindow } from 'electron';

    const { port1, port2 } = new MessageChannelMain();

    // port1 留在主进程
    ipcMain.on('init-port', (event) => {
    // renderer 端触发初始化时把 port2 转交
    event.ports[0]?.postMessage({ type: 'init' });
    event.ports[0]?.start();
    });

    // 主进程直接挂监听
    port1.on('message', (event) => {
    const { id, payload } = event.data;
    // 处理逻辑
    const data = require('./mock-data.json');
    port1.postMessage({ id, echo: payload, size: data.length });
    });
    port1.start();

    // renderer 端
    const { ipcRenderer } = require('electron');

    // 一次性建链
    ipcRenderer.on('port', (event) => {
    const port = event.ports[0];
    port.on('message', (e) => {
    // 处理主进程回执
    console.log('主进程回执:', e.data);
    });
    port.start();

    // 业务调用直接走 port
    window.electronAPI.sendViaPort = (payload) => {
    return new Promise((resolve) => {
    const id = Date.now() + Math.random();
    const handler = (e: MessageEvent) => {
    if (e.data.id === id) {
    port.removeEventListener('message', handler);
    resolve(e.data);
    }
    };
    port.addEventListener('message', handler);
    port.postMessage({ id, payload });
    });
    };
    });

    我用 MessagePort 重新跑同一组 benchmark:

    平台p50p95p99
    Windows 11 (invoke) 3.8ms 5.2ms 7.1ms
    鸿蒙 PC (invoke) 28.4ms 33.1ms 41.7ms
    鸿蒙 PC (MessagePort) 4.6ms 6.1ms 8.3ms

    5ms。和 Windows 几乎齐平。MessagePort 绕过了那个桥接层,消息直接在 renderer 进程和主进程之间走 mojo 端口,跨进程序列化只有一次。

    顺便说一句,这种"跨平台性能断层"问题用模拟器是测不出来的。我是在鸿蒙 PC 真机上用 Performance Observer 抓 IPC 事件才发现 invoke 内部的桥接调用链特别长。模拟器上鸿蒙和 Windows 都跑得飞快,掩盖了真实差距。

    怎么无侵入替换现有 invoke/handle

    等一下,我漏说一个关键点——我项目里已经有 200 多处 ipcRenderer.invoke 调用了,不可能一个个改成 MessagePort。我的做法是写一个兼容层:

    // ipc-compat.ets — 兼容层,新代码用 MessagePort,旧代码走 invoke
    import { ipcRenderer } from 'electron';

    interface IPCBridge {
    invoke(channel: string, args: any[]): Promise<any>;
    useMessagePort: boolean;
    }

    class MessagePortBridge implements IPCBridge {
    useMessagePort = true;
    private port?: MessagePort;
    private pendingMap = new Map<string, { resolve: Function; reject: Function }>();

    constructor() {
    ipcRenderer.on('port', (event) => {
    this.port = event.ports[0];
    this.port.on('message', (e) => {
    const { id, result, error } = e.data;
    const pending = this.pendingMap.get(id);
    if (pending) {
    if (error) pending.reject(new Error(error));
    else pending.resolve(result);
    this.pendingMap.delete(id);
    }
    });
    this.port.start();
    });
    }

    async invoke(channel: string, args: any[]): Promise<any> {
    if (!this.port) {
    // 还没建好链,临时走 invoke 兜底
    return ipcRenderer.invoke(channel, args);
    }
    const id = `${channel}_${Date.now()}_${Math.random()}`;
    return new Promise((resolve, reject) => {
    this.pendingMap.set(id, { resolve, reject });
    this.port!.postMessage({ id, channel, args });
    });
    }
    }

    class InvokeBridge implements IPCBridge {
    useMessagePort = false;
    async invoke(channel: string, args: any[]): Promise<any> {
    return ipcRenderer.invoke(channel, args);
    }
    }

    // 平台判断:鸿蒙用 MessagePort,其他平台用 invoke
    const isHarmonyDesktop = navigator.userAgent.includes('HarmonyOS') &&
    process.platform === 'linux'; // 鸿蒙 PC 底层是 Linux 内核

    export const bridge: IPCBridge = isHarmonyDesktop
    ? new MessagePortBridge()
    : new InvokeBridge();

    然后全局替换 window.electronAPI.invoke 指向 bridge.invoke 即可。Windows 上的代码一行不用动,鸿蒙 PC 自动走 MessagePort。

    那个我差点推倒的架构

    我前面说"差点把架构推倒重来"是真的。当时我看到 33ms 的数据,脑子里第一反应是把窗口间通信换成 WebSocket——本机起一个 ws server,所有窗口连上去。我同事听了我的方案,沉默了几秒说"你冷静一下,这 IPC 不是网络问题"。幸亏他拦住了,不然我真会在 Electron 里起一个本地 WebSocket,然后第二天再花三天删掉。

    后来我反思了一下这次"差点翻车"的过程:

  • 不要只看绝对数字。33ms 在 Electron 应用里确实算慢,但慢在哪儿需要拆。我一开始只盯着"鸿蒙 PC 慢"这个现象,没有分拆 IPC 链路各段的耗时。
  • 跨平台性能问题优先怀疑 IPC/IO。纯 CPU 慢和 IO 慢的修复方向完全不同,先做 baseline 测试能少走很多弯路。
  • 别用模拟器做性能测试。我前面已经提过,这里再强调一遍,模拟器是另一个 Linux 内核环境,跑出来什么数据都不代表真机。
  • Web 标准 API 往往比框架封装 API 性能更好。MessagePort 是 Web 标准,invoke/handle 是 Electron 封装——通常 Web 标准更接近底层,少一层封装就少一次开销。这条经验我以后会用到所有 Electron 性能调优场景里。
  • 我现在已经把雷达鸭桌面版的所有窗口间通信都迁到 MessagePort 了,鸿蒙 PC 上从 33ms 降到 5ms,Windows 上从 4ms 降到 4.6ms——略有损耗是因为多了一次 Promise 包装,但能接受。

    等鸿蒙 PC 真正放开 Electron 上架的时候,我会再用真机跑一轮 benchmark 确认。话说回来,鸿蒙的 Electron 生态现在还在早期,官方对 MessageChannelMain 内部桥接层的优化空间应该还有不少,希望下个版本能直接把这个损耗抹平,不然每个跨平台 Electron 应用都得自己重写 IPC 层,累。

    你遇到过类似的"换平台突然慢一个数量级"的问题吗?留言聊聊,说不定我下一篇就分析你的场景。


    关于作者:老三,10+ 年软件开发老兵,软件设计师 & 人工智能应用工程师,专注鸿蒙 ArkTS 北向开发 + Web 前端,偶尔折腾 Electron 跨端桌面应用。不定期在 CSDN 分享鸿蒙、AI、性能调优方向的真实踩坑记录。

    本文遵循 MIT 协议,转载请注明出处。

    赞(0)
    未经允许不得转载:171主机测评 » Electron 跑在鸿蒙 PC 上,窗口通信居然比 Windows 慢 8 倍?我差点把架构推倒重来
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址