Electron 在鸿蒙 PC 上启动慢?我把冷启动从 7 秒压到 1.5 秒的完整记录

Electron 在鸿蒙 PC 上启动慢?我把冷启动从 7 秒压到 1.5 秒的完整记录

上周把 Electron 应用移植到鸿蒙 PC 上跑了一圈,结果用户反馈说"启动慢得像在加载 PS"。我掐表测了一下,从双击图标到主窗口完全可交互,居然要 7 秒多。这放在 Windows 上也就 2 秒出头,鸿蒙 PC 怎么就这么拉胯?我花了三天时间用各种手段往下压,最终把冷启动干到了 1.5 秒左右。下面把完整过程记录下来,避免你们再走弯路。

先搞清楚时间花在哪了

拿到性能问题,我的习惯是不打无准备之仗。Electron 启动链路很长:主进程初始化 → 创建 BrowserWindow → 加载入口 HTML → 渲染进程启动 → 预加载脚本执行 → 前端框架初始化 → 首屏渲染完成。到底哪一步在鸿蒙 PC 上慢得离谱?不量化一下就是瞎猜。

我在主进程和渲染进程里埋了一堆 performance.markconsole.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-readyapp-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 慢不少(我猜测跟文件系统实现有关)。这一堆模块里,updateCheckertrayManagerharmonyosBridge 在应用启动阶段根本用不上,它们完全可以等到窗口创建完成后再加载。

优化策略很简单:把非核心模块改成懒加载

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 协议,转载请注明出处。欢迎转载,但请保留原文链接及作者信息。

相关推荐
leon_teacher3 小时前
HarmonyOS 6 古诗学习宝实战:基于 Preferences 实现错题本自动派生与题级去重系统
学习·华为·harmonyos
key_3_feng3 小时前
鸿蒙6.1.1 (API 24) 架构深度解析
华为·架构·harmonyos
Swift社区4 小时前
鸿蒙 PC 性能优化实战:从卡顿到丝滑
华为·性能优化·harmonyos
痕忆丶4 小时前
openharmony源码编译之窗口管理屏幕适配
harmonyos
敲代码的鱼哇4 小时前
NFC读卡能力 支持安卓/iOS/鸿蒙 UTS插件
android·ios·harmonyos
爱吃大芒果18 小时前
从零搭建完整 HarmonyOS 应用实战教程
华为·typescript·harmonyos
richard_yuu18 小时前
鸿蒙首页实战开发|ArkTS 从零搭建治愈系首页、动态问候与功能模块
华为·harmonyos
音视频牛哥1 天前
SmartMediaKit 鸿蒙NEXT GB28181设备接入SDK
华为·harmonyos·鸿蒙gb28181·鸿蒙next gb28181·鸿蒙gb28181接入·鸿蒙接入gb28181平台·鸿蒙执法记录仪gb28181
Momo__1 天前
Electron应用性能优化:从启动慢到秒开的7个实战技巧
前端·electron