Electron 在鸿蒙 PC 上启动慢?我把冷启动从 7 秒压到 1.5 秒的完整记录
上周把 Electron 应用移植到鸿蒙 PC 上跑了一圈,结果用户反馈说"启动慢得像在加载 PS"。我掐表测了一下,从双击图标到主窗口完全可交互,居然要 7 秒多。这放在 Windows 上也就 2 秒出头,鸿蒙 PC 怎么就这么拉胯?我花了三天时间用各种手段往下压,最终把冷启动干到了 1.5 秒左右。下面把完整过程记录下来,避免你们再走弯路。
先搞清楚时间花在哪了
拿到性能问题,我的习惯是不打无准备之仗。Electron 启动链路很长:主进程初始化 → 创建 BrowserWindow → 加载入口 HTML → 渲染进程启动 → 预加载脚本执行 → 前端框架初始化 → 首屏渲染完成。到底哪一步在鸿蒙 PC 上慢得离谱?不量化一下就是瞎猜。
我在主进程和渲染进程里埋了一堆 performance.mark 和 console.time,写了个简单的打点模块:
javascript
// utils/perf-timer.js
const { performance } = require('perf_hooks');
const fs = require('fs');
const path = require('path');
class PerfTimer {
constructor() {
this.marks = [];
this.logFile = path.join(require('electron').app.getPath('userData'), 'startup-perf.json');
}
mark(label) {
const time = performance.now();
this.marks.push({ label, time: Math.round(time * 100) / 100 });
console.timeLog ? console.timeLog('startup', label) : console.log(`[startup] ${label}: ${time.toFixed(2)}ms`);
}
save() {
fs.writeFileSync(this.logFile, JSON.stringify(this.marks, null, 2));
}
diff(from, to) {
const a = this.marks.find(m => m.label === from);
const b = this.marks.find(m => m.label === to);
return a && b ? (b.time - a.time).toFixed(2) + 'ms' : 'N/A';
}
}
module.exports = new PerfTimer();
主进程入口 main.js 里这样用:
javascript
const perf = require('./utils/perf-timer');
const { app, BrowserWindow } = require('electron');
perf.mark('app-before-ready');
app.whenReady().then(() => {
perf.mark('app-ready');
const win = new BrowserWindow({
width: 1280,
height: 800,
show: false,
webPreferences: {
preload: require('path').join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
perf.mark('window-created');
win.loadFile('index.html');
perf.mark('load-file-called');
win.webContents.on('did-finish-load', () => {
perf.mark('did-finish-load');
win.show();
perf.mark('window-shown');
perf.save();
// 输出各阶段耗时
console.log('=== 启动耗时分析 ===');
console.log('主进程准备:', perf.diff('app-before-ready', 'app-ready'));
console.log('窗口创建:', perf.diff('app-ready', 'window-created'));
console.log('页面加载:', perf.diff('window-created', 'did-finish-load'));
console.log('总耗时:', perf.diff('app-before-ready', 'window-shown'));
});
});
跑了几遍取平均值,结果让我有点意外。主进程初始化(app-before-ready 到 app-ready)居然占了 3.8 秒,页面加载才 2.1 秒,窗口创建反倒只有 200 毫秒。看来主进程启动才是罪魁祸首,而不是我以为的渲染进程慢。
第一刀:砍掉主进程的 require 洪水
打开主进程的 main.js,我发现自己犯了一个低级错误------为了图省事,把所有模块都在文件顶部一次性 require 了:
javascript
// 优化前 ------ 典型的 require 洪水
const { app, BrowserWindow, ipcMain, dialog, Menu, Tray, nativeImage } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
const log = require('./utils/logger');
const config = require('./config/app-config');
const updateChecker = require('./services/update-checker');
const trayManager = require('./services/tray-manager');
const menuBuilder = require('./services/menu-builder');
const shortcutManager = require('./services/shortcut-manager');
const harmonyosBridge = require('./native/harmonyos-bridge');
问题出在哪?Electron 打包后,Node.js 的模块解析在鸿蒙 PC 上比 Windows 慢不少(我猜测跟文件系统实现有关)。这一堆模块里,updateChecker、trayManager、harmonyosBridge 在应用启动阶段根本用不上,它们完全可以等到窗口创建完成后再加载。
优化策略很简单:把非核心模块改成懒加载:
javascript
// 优化后 ------ 只保留启动必须的模块
const { app, BrowserWindow } = require('electron');
const path = require('path');
// 延迟加载:这些模块启动阶段不需要
let updateChecker, trayManager, menuBuilder, shortcutManager, harmonyosBridge;
function getUpdateChecker() {
if (!updateChecker) updateChecker = require('./services/update-checker');
return updateChecker;
}
function getHarmonyosBridge() {
if (!harmonyosBridge) harmonyosBridge = require('./native/harmonyos-bridge');
return harmonyosBridge;
}
app.whenReady().then(() => {
// 只创建窗口,其他服务延迟初始化
createWindow();
// 窗口显示后 500ms 再初始化非核心服务
setTimeout(() => {
getUpdateChecker().check();
getHarmonyosBridge().init();
}, 500);
});
这一刀砍下去,主进程初始化从 3.8 秒降到 1.9 秒。效果立竿见影,但也引入了一个坑------harmonyosBridge 内部在 require 时会执行一段原生模块的探测逻辑,延迟加载确实快了,但如果在探测完成前就有 IPC 调用进来,会直接报错。我后来给它加了个 Promise 锁才解决:
javascript
// native/harmonyos-bridge.js
let initPromise = null;
function init() {
if (initPromise) return initPromise;
initPromise = new Promise((resolve) => {
// 原生模块探测逻辑...
setTimeout(() => resolve(true), 100);
});
return initPromise;
}
function callNative(method, args) {
return init().then(() => {
// 实际调用...
});
}
module.exports = { init, callNative };
第二刀:预加载脚本瘦身
预加载脚本 (preload.js) 是 Electron 的安全桥梁,但它会在每个渲染进程创建时执行。我的 preload.js 最初有 400 多行,里面塞了一大堆 IPC 通道注册、工具函数、甚至还有一些常量配置。在鸿蒙 PC 上,这段脚本的执行时间比 Windows 长了将近一倍。
我的做法是把 preload.js 拆成"核心必加载"和"按需注入"两部分:
javascript
// preload.js ------ 只保留最核心的 API 暴露
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// 只有这 3 个是首屏必须用到的
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
on: (channel, callback) => ipcRenderer.on(channel, callback),
platform: process.platform
});
那些不急着用的 API,改成在渲染进程里动态请求:
javascript
// renderer.js ------ 首屏渲染完成后再加载扩展 API
async function loadExtendedAPI() {
const extended = await window.electronAPI.invoke('get-extended-api-manifest');
// 按需注册...
}
window.addEventListener('DOMContentLoaded', () => {
renderApp(); // 先渲染核心界面
setTimeout(loadExtendedAPI, 0); // 空闲时加载扩展
});
预加载脚本从 400 行砍到 30 行,执行时间从 800 毫秒降到 120 毫秒。这里有个细节:鸿蒙 PC 的 Chromium 版本可能跟 Windows 有差异,contextBridge 的序列化性能表现不太一致,所以尽量减少传递的数据量总没错。
第三刀:加个 Splash 屏做"心理加速"
说实话,Splash 屏并不能真正减少启动时间,但它能改变用户对启动时间的感知。我从双击图标到窗口显示之间有 1 秒多的空窗期,用户会怀疑"是不是点错了"。加一个轻量的 Splash 窗口,让用户立刻看到反馈,体验好很多。
javascript
// splash.js
const { BrowserWindow } = require('electron');
const path = require('path');
let splash = null;
function showSplash() {
splash = new BrowserWindow({
width: 400,
height: 300,
frame: false,
alwaysOnTop: true,
transparent: true,
skipTaskbar: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});
splash.loadFile('splash.html');
}
function closeSplash() {
if (splash && !splash.isDestroyed()) {
splash.close();
splash = null;
}
}
module.exports = { showSplash, closeSplash };
在 main.js 里调用:
javascript
const { showSplash, closeSplash } = require('./splash');
app.whenReady().then(() => {
showSplash(); // 立刻显示 Splash
const mainWindow = new BrowserWindow({
show: false, // 先不显示主窗口
// ...
});
mainWindow.loadFile('index.html');
mainWindow.webContents.on('did-finish-load', () => {
mainWindow.show();
closeSplash(); // 主窗口就绪后关闭 Splash
});
});
splash.html 要尽量轻量------我只放了一个 CSS 动画和一个 logo,没有任何 JS 框架。文件大小控制在 15KB 以内,加载几乎是瞬时的。
第四刀:鸿蒙 PC 特有的进程优先级调优
这一步是我踩坑最狠的地方。我查到鸿蒙 PC(OpenHarmony 桌面版)支持通过 ohos.app 相关 API 设置进程优先级,于是兴冲冲地在主进程里加了一段代码,想把渲染进程的优先级调高:
javascript
// 坑货代码,不要直接抄!
const { exec } = require('child_process');
exec('renice -n -5 -p ' + process.pid); // 在鸿蒙 PC 上这段直接报错
鸿蒙 PC 的进程管理跟 Linux 并不完全一样,renice 命令不存在,而且沙箱权限也不允许随意修改优先级。我反复试了五六次,查阅了华为开发者论坛的零散帖子,最后发现 Electron 在鸿蒙 PC 上跑的是基于 OpenHarmony 的容器环境,进程调度由系统统一管控,应用层没法直接干预。
不过也不是完全没办法。虽然不能调优先级,但可以通过减少进程数量来降低调度开销。Electron 默认会为每个窗口创建一个独立的渲染进程,如果你的应用有多个隐藏的 Background Window,它们会吃掉不少启动资源。我把几个不急着用的后台服务合并到了一个专门的隐藏窗口里,减少了两个多余的渲染进程:
javascript
// 合并后台服务到一个隐藏窗口
const backgroundWindow = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: path.join(__dirname, 'preload-background.js')
}
});
backgroundWindow.loadFile('background.html');
// background.html 内部通过 iframe 或模块化方式加载各个后台服务
这一招减少了进程创建开销,主进程初始化又快了 300 毫秒左右。
优化效果汇总
我把优化前后的数据整理了一下,对比相当明显:
| 阶段 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 主进程初始化 | 3.8s | 1.1s | -71% |
| 预加载脚本执行 | 0.8s | 0.12s | -85% |
| 页面加载+渲染 | 2.1s | 1.8s | -14% |
| 窗口显示延迟 | 0.5s | 0.08s | -84% |
| 用户感知启动时间 | ~7.2s | ~1.5s | -79% |
用户感知启动时间指的是"从双击图标到界面可交互"的完整时间。Splash 屏的贡献主要体现在"心理层面"------用户不再觉得那 1.5 秒很难熬。
几个需要注意的坑
优化启动速度的过程中,我还遇到了一些杂七杂八的问题,一并记下来:
不要把所有优化手段一次性堆上去。我一开始同时改了 require 策略、预加载瘦身、还加了个 V8 缓存,结果应用在鸿蒙 PC 上直接白屏了。排查了两个小时才发现是 V8 快照缓存跟鸿蒙的 Chromium 版本不兼容。建议每改一个优化点就测一遍,稳扎稳打。
Splash 屏的关闭时机要把握好 。如果主窗口还没完全渲染就关闭 Splash,会出现一段"桌面空白期";如果关太晚,用户会觉得 Splash 碍事。我的做法是在 did-finish-load 事件触发后延迟 150 毫秒再关闭,给首次渲染留一点缓冲。
鸿蒙 PC 的 userData 路径跟 Windows 差异很大,如果你在启动阶段读写配置文件,路径解析可能耗时。建议把配置缓存到内存里,避免重复读盘。
写在最后
Electron 应用在鸿蒙 PC 上的启动性能问题,本质上不是 Electron 本身的锅,而是跨平台移植时各种环境差异叠加的结果。文件系统性能、进程调度策略、Chromium 版本差异,这些因素单独看都不致命,但凑在一起就能把启动时间拖垮。我的建议是:先量化、再动手 ,用 performance.mark 找到真正的瓶颈,别像我一样一开始瞎猜是渲染进程慢,结果在主进程上浪费了半天时间。
如果你也在做 Electron + 鸿蒙 PC 的适配,欢迎评论区交流。这块目前资料不多,基本都是靠开发者互相填坑。
本文遵循 MIT 协议,转载请注明出处。欢迎转载,但请保留原文链接及作者信息。