Electron

第十章:性能优化与诊断

第十章:性能优化与诊断

目录


性能三要素

Electron 应用的性能优化围绕三个核心指标:

┌──────────────────────────────────────────────────────────────┐
│                  Electron 性能三角                            │
│                                                              │
│                    启动速度                                   │
│                   ╱        ╲                                  │
│                  ╱          ╲                                 │
│                 ╱   用户感知  ╲                               │
│                ╱    的性能     ╲                              │
│               ╱                ╲                              │
│         运行时响应 ──────── 内存占用                           │
│                                                              │
│  启动速度:   从双击图标到窗口可交互的时间                     │
│  运行时响应:  操作反馈的即时性(点击、滚动、输入)            │
│  内存占用:   长时间运行后的内存稳定性                         │
│                                                              │
│  用户感知基准:                                               │
│  ┌────────────┬────────────┬────────────────────┐            │
│  │ 指标       │ 良好       │ 需要优化            │            │
│  ├────────────┼────────────┼────────────────────┤            │
│  │ 冷启动     │ < 3 秒     │ > 5 秒              │            │
│  │ 操作响应   │ < 100 ms   │ > 300 ms            │            │
│  │ 内存占用   │ < 200 MB   │ > 500 MB            │            │
│  └────────────┴────────────┴────────────────────┘            │
└──────────────────────────────────────────────────────────────┘

Electron 官方性能文档总结了 8 条核心建议,我们逐一展开。


启动优化

1. 延迟加载模块 — 不要在启动时贪婪 require

这是 Electron 官方最强调的一条:Carelessly including modules

问题:启动时加载所有模块

  // main.js — 错误示范 ❌
  const fs = require('fs');
  const path = require('path');
  const crypto = require('crypto');
  const sharp = require('sharp');           // 大型原生模块
  const sqlite3 = require('better-sqlite3'); // 大型原生模块
  const marked = require('marked');          // Markdown 解析
  const hljs = require('highlight.js');      // 代码高亮

  启动时间线:
  ┌─────────────────────────────────────────────┐
  │ require sharp     ████████  120ms           │
  │ require sqlite3   ██████    90ms            │
  │ require marked    ███       45ms            │
  │ require hljs      █████     75ms            │
  │                              ─────          │
  │                    总计:     330ms 浪费!     │
  │                                             │
  │ 这些模块在用户打开文件之前根本不需要         │
  └─────────────────────────────────────────────┘

解决方案:用到时再加载

// main.js — 正确做法 ✅

// 启动时只加载必要模块
const { app, BrowserWindow } = require('electron');

// 其他模块延迟加载
let _sharp;
function getSharp() {
  if (!_sharp) {
    _sharp = require('sharp');  // 首次调用时才加载
  }
  return _sharp;
}

// IPC handler 中按需使用
ipcMain.handle('image:resize', async (event, imagePath, width) => {
  const sharp = getSharp();  // 用户真正需要时才加载
  return sharp(imagePath).resize(width).toBuffer();
});

更优雅的写法 — 用 ESM 动态 import:

// 使用动态 import(推荐)
ipcMain.handle('markdown:render', async (event, text) => {
  const { marked } = await import('marked');  // 按需动态加载
  return marked(text);
});

2. 预热窗口 — Hidden Window Trick

窗口创建和页面加载是启动过程中最耗时的部分。通过预先创建隐藏窗口来消除感知延迟:

传统方式(用户可见白屏):

  双击图标 → [创建窗口 200ms] → [加载HTML 300ms] → [渲染 200ms] → 显示

              ◄──────── 用户看到白屏 700ms ────────────────────────►│

预热方式(窗口就绪后才显示):

  双击图标 → [创建隐藏窗口] → [加载HTML] → [渲染完成] → 显示!
                │                                          │
                └── 用户看到的:启动画面 / 无感知 ──────────┘
// main.js — 预热窗口
function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    show: false,              // 关键:先不显示
    backgroundColor: '#1e1e1e', // 避免白色闪烁
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  win.loadFile('index.html');

  // 等页面完全渲染后再显示
  win.once('ready-to-show', () => {
    win.show();
  });

  return win;
}

3. V8 Snapshot 与 Code Cache

V8 引擎支持将编译后的字节码缓存起来,减少下次启动的解析时间:

首次启动:
  源代码.js → [解析] → [编译为字节码] → [执行]
              ████████████████████████
              较慢

后续启动(有 Code Cache):
  缓存的字节码 → [直接执行]
                  ████████
                  快得多
// Electron 自动管理渲染进程的 code cache
// 主进程可以通过 v8-compile-cache 加速
// 安装: npm install v8-compile-cache

// main.js 顶部
require('v8-compile-cache');

// Electron 还支持自定义 V8 snapshot(高级用法)
// 适合大型应用,将初始化代码打入 snapshot
// 参考:electron-link + mksnapshot

4. 启动性能度量

// main.js — 记录启动各阶段耗时
const startTime = Date.now();

app.on('ready', () => {
  console.log(`app ready: ${Date.now() - startTime}ms`);

  const win = createWindow();

  win.webContents.once('dom-ready', () => {
    console.log(`DOM ready: ${Date.now() - startTime}ms`);
  });

  win.webContents.once('did-finish-load', () => {
    console.log(`页面加载完成: ${Date.now() - startTime}ms`);
  });

  win.once('ready-to-show', () => {
    console.log(`可以显示: ${Date.now() - startTime}ms`);
    win.show();
    console.log(`总启动时间: ${Date.now() - startTime}ms`);
  });
});

运行时优化

1. 不要阻塞主进程

这是 Electron 官方反复强调的一条:Blocking the main process

主进程负责窗口管理、IPC 调度和系统事件响应。如果主进程被长任务阻塞,整个应用会卡死:

主进程被阻塞的效果:

  主进程:  [处理IPC] [████████ CPU密集计算 ████████] [处理IPC]
                      │                              │
  窗口响应: 正常      │   冻结!不响应鼠标和键盘     │ 恢复
                      │                              │
  用户感受:           "应用卡死了"                    "终于好了"

解决方案:长任务放到 Worker 或 Utility Process

// ❌ 错误:在主进程中做 CPU 密集计算
ipcMain.handle('data:analyze', (event, data) => {
  // 这会阻塞主进程!
  const result = heavyComputation(data);
  return result;
});

// ✅ 方案一:使用 Worker Thread
const { Worker } = require('worker_threads');

ipcMain.handle('data:analyze', (event, data) => {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./workers/analyzer.js', {
      workerData: data,
    });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
});

// ✅ 方案二:使用 Utility Process(Electron 推荐)
const { utilityProcess } = require('electron');

let analyzerProcess;

function getAnalyzer() {
  if (!analyzerProcess) {
    analyzerProcess = utilityProcess.fork(
      path.join(__dirname, 'workers/analyzer.js')
    );
  }
  return analyzerProcess;
}

ipcMain.handle('data:analyze', (event, data) => {
  return new Promise((resolve) => {
    const analyzer = getAnalyzer();
    analyzer.postMessage({ type: 'analyze', data });
    analyzer.once('message', (result) => resolve(result));
  });
});

2. IPC 批量化

频繁的跨进程通信有开销。如果渲染进程需要大量数据,批量传输比逐条请求高效得多:

❌ 逐条请求(每条 IPC 都有序列化/反序列化开销):

  渲染进程        主进程
  getItem(1) ──► 查询 ──► 返回
  getItem(2) ──► 查询 ──► 返回
  getItem(3) ──► 查询 ──► 返回
  ...
  getItem(100) ─► 查询 ──► 返回     总计: 100次 IPC × ~0.5ms = 50ms

✅ 批量请求:

  渲染进程                  主进程
  getItems([1..100]) ────► 批量查询 ──► 返回全部     总计: 1次 IPC = ~2ms
// ❌ 逐条 IPC
async function loadAllItems() {
  const items = [];
  for (const id of ids) {
    const item = await window.electronAPI.getItem(id);
    items.push(item);
  }
  return items;
}

// ✅ 批量 IPC
async function loadAllItems() {
  return window.electronAPI.getItems(ids); // 一次传输
}

// 主进程 handler
ipcMain.handle('db:getItems', (event, ids) => {
  // 一次查询返回所有结果
  const placeholders = ids.map(() => '?').join(',');
  return db.prepare(`SELECT * FROM items WHERE id IN (${placeholders})`).all(...ids);
});

3. 渲染进程优化

渲染进程就是一个 Chromium 页面,Web 性能优化的经验完全适用:

// 虚拟滚动 — 大列表只渲染可见部分
// 使用 react-virtualized、vue-virtual-scroller 等库
// 原理:
//
// 传统列表(10000 项全部渲染):
//   DOM 节点: 10000 个  → 内存高、渲染慢
//
// 虚拟滚动(只渲染可见的 ~20 项):
//   ┌──────────────┐
//   │ 缓冲区 (5项)  │ ← 即将滚入视野
//   │ ════════════ │
//   │ 可见区 (10项) │ ← 用户看到的
//   │ ════════════ │
//   │ 缓冲区 (5项)  │ ← 刚滚出视野
//   └──────────────┘
//   DOM 节点: ~20 个   → 内存低、渲染快

// requestIdleCallback — 在浏览器空闲时执行低优先级任务
function processBackgroundTasks(tasks) {
  function doWork(deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0) {
      const task = tasks.shift();
      task();
    }
    if (tasks.length > 0) {
      requestIdleCallback(doWork);
    }
  }
  requestIdleCallback(doWork);
}

// 示例:空闲时预加载缩略图
const thumbnailTasks = fileList.map(file => () => {
  preloadThumbnail(file);
});
processBackgroundTasks(thumbnailTasks);

内存诊断

Chrome DevTools Memory 面板

Electron 内置了完整的 Chrome DevTools,是诊断内存问题的首选工具:

打开方式:
  开发时: win.webContents.openDevTools()
  运行时: Ctrl+Shift+I (Windows/Linux) 或 Cmd+Option+I (macOS)

Memory 面板功能:

  ┌─────────────────────────────────────────────────────┐
  │  Memory                                              │
  │                                                      │
  │  ○ Heap snapshot      — 堆快照,查看内存中所有对象   │
  │  ○ Allocation timeline — 分配时间线,发现分配高峰    │
  │  ○ Allocation sampling — 采样,找出分配热点函数      │
  │                                                      │
  │  诊断步骤:                                          │
  │  1. 拍摄快照 A(操作前)                             │
  │  2. 执行怀疑有泄漏的操作                             │
  │  3. 拍摄快照 B(操作后)                             │
  │  4. 对比两次快照,找增长的对象                       │
  └─────────────────────────────────────────────────────┘

process.memoryUsage() 监控

// main.js — 定期上报内存使用
function logMemory(label) {
  const mem = process.memoryUsage();
  console.log(`[Memory:${label}]`, {
    rss: `${(mem.rss / 1024 / 1024).toFixed(1)} MB`,       // 常驻内存
    heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(1)} MB`, // V8 堆已用
    heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(1)} MB`, // V8 堆总量
    external: `${(mem.external / 1024 / 1024).toFixed(1)} MB`,  // C++ 对象
  });
}

// 定期检测
setInterval(() => logMemory('periodic'), 30000);

// 关键节点检测
app.on('ready', () => logMemory('app-ready'));
app.on('browser-window-created', () => logMemory('window-created'));

渲染进程也可以监控:

// renderer.js — 使用 Performance API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'measure') {
      console.log(`${entry.name}: ${entry.duration.toFixed(1)}ms`);
    }
  }
});
observer.observe({ entryTypes: ['measure'] });

// 监控内存(需要开启 performance.measureUserAgentSpecificMemory)
async function checkRendererMemory() {
  if (performance.measureUserAgentSpecificMemory) {
    const result = await performance.measureUserAgentSpecificMemory();
    console.log('渲染进程内存:', (result.bytes / 1024 / 1024).toFixed(1), 'MB');
  }
}

常见内存泄漏模式

┌──────────────────────────────────────────────────────────────┐
│                  常见内存泄漏模式                             │
│                                                              │
│  1. 事件监听未移除                                           │
│     ┌──────────────────────────────────────────────┐        │
│     │ // ❌ 泄漏:每次调用都新增监听器             │        │
│     │ function setup() {                            │        │
│     │   ipcMain.on('data', handler);  // 累积!     │        │
│     │ }                                             │        │
│     │                                               │        │
│     │ // ✅ 修复:先移除再添加,或用 once           │        │
│     │ function setup() {                            │        │
│     │   ipcMain.removeListener('data', handler);   │        │
│     │   ipcMain.on('data', handler);               │        │
│     │ }                                             │        │
│     └──────────────────────────────────────────────┘        │
│                                                              │
│  2. 闭包持有大对象                                           │
│     ┌──────────────────────────────────────────────┐        │
│     │ // ❌ 泄漏:闭包持有 largeData 引用          │        │
│     │ function processFile(path) {                  │        │
│     │   const largeData = fs.readFileSync(path);   │        │
│     │   return () => {                              │        │
│     │     // 即使只用 largeData.length              │        │
│     │     return largeData.length; // 但整个对象被持有│       │
│     │   };                                          │        │
│     │ }                                             │        │
│     │                                               │        │
│     │ // ✅ 修复:只保留需要的值                    │        │
│     │ function processFile(path) {                  │        │
│     │   const largeData = fs.readFileSync(path);   │        │
│     │   const size = largeData.length;             │        │
│     │   // largeData 可被 GC 回收                  │        │
│     │   return () => size;                          │        │
│     │ }                                             │        │
│     └──────────────────────────────────────────────┘        │
│                                                              │
│  3. BrowserWindow 未正确销毁                                 │
│     ┌──────────────────────────────────────────────┐        │
│     │ // ❌ 泄漏:窗口关闭但引用未释放              │        │
│     │ let settingsWindow = new BrowserWindow({...});│        │
│     │ // 窗口关闭后,settingsWindow 仍然引用已销毁对象│       │
│     │                                               │        │
│     │ // ✅ 修复:监听 closed 事件,置空引用        │        │
│     │ settingsWindow.on('closed', () => {          │        │
│     │   settingsWindow = null;                     │        │
│     │ });                                           │        │
│     └──────────────────────────────────────────────┘        │
│                                                              │
│  4. 定时器未清理                                             │
│     ┌──────────────────────────────────────────────┐        │
│     │ // ❌ 页面卸载但 setInterval 仍在运行         │        │
│     │ setInterval(pollServer, 5000);               │        │
│     │                                               │        │
│     │ // ✅ 修复:在适当时机清除                    │        │
│     │ const timer = setInterval(pollServer, 5000); │        │
│     │ window.addEventListener('beforeunload', () => │       │
│     │   clearInterval(timer);                       │        │
│     │ });                                           │        │
│     └──────────────────────────────────────────────┘        │
└──────────────────────────────────────────────────────────────┘

性能分析工具

1. Chrome Tracing

Chrome Tracing 是最底层的性能分析工具,可以捕获 Chromium 内核的所有事件:

使用方式:
  1. 在 Electron 中打开: chrome://tracing
  2. 点击 Record → 选择分类 → 操作应用 → Stop
  3. 分析时间线

  ┌─────────────────────────────────────────────────┐
  │  chrome://tracing 时间线                         │
  │                                                  │
  │  Browser Process  ┃████░░████░░░████░░░░░░      │
  │  GPU Process      ┃░░░░████░░░░░░████░░░░      │
  │  Renderer (pid)   ┃██████████░░░░░░██████      │
  │                   ┃                             │
  │  ─────────────────┃─────────────────────── t    │
  │                   0ms               500ms       │
  └─────────────────────────────────────────────────┘

2. Electron contentTracing 模块

用代码控制追踪,适合自动化性能分析:

const { contentTracing } = require('electron');

async function traceStartup() {
  // 开始追踪
  await contentTracing.startRecording({
    included_categories: ['*'],  // 追踪所有分类
    // 或精确指定:['v8', 'blink', 'cc', 'gpu']
  });

  // ... 执行需要分析的操作 ...

  // 停止追踪,保存文件
  const path = await contentTracing.stopRecording();
  console.log('追踪文件已保存到:', path);
  // 在 chrome://tracing 中打开此文件分析
}

app.whenReady().then(traceStartup);

3. Performance Observer API(渲染进程)

// renderer.js — 监控长任务
const longTaskObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // 超过 50ms 的任务被视为 "长任务"
    console.warn(`⚠️ 长任务检测: ${entry.duration.toFixed(1)}ms`, entry);
  }
});
longTaskObserver.observe({ entryTypes: ['longtask'] });

// 监控自定义性能指标
performance.mark('render-start');
// ... 渲染操作 ...
performance.mark('render-end');
performance.measure('渲染耗时', 'render-start', 'render-end');

4. 主进程性能追踪

// main.js — 用 perf_hooks 分析主进程
const { PerformanceObserver, performance } = require('perf_hooks');

const obs = new PerformanceObserver((items) => {
  items.getEntries().forEach((entry) => {
    console.log(`[Perf] ${entry.name}: ${entry.duration.toFixed(1)}ms`);
  });
});
obs.observe({ entryTypes: ['measure'] });

// 包装 IPC handler,自动测量耗时
function measuredHandle(channel, handler) {
  ipcMain.handle(channel, async (event, ...args) => {
    performance.mark(`${channel}-start`);
    const result = await handler(event, ...args);
    performance.mark(`${channel}-end`);
    performance.measure(channel, `${channel}-start`, `${channel}-end`);
    return result;
  });
}

// 使用
measuredHandle('db:query', async (event, sql) => {
  return db.prepare(sql).all();
});

常见问题

1. 应用启动慢(> 5 秒)

排查清单:
  □ 是否在启动时加载了大型原生模块?  → 延迟加载
  □ 是否在 main.js 顶部 require 了很多模块? → 按需 import
  □ 渲染页面是否加载了大型 JS bundle? → 代码分割
  □ 是否在 ready 事件前做了网络请求? → 移到 ready 之后
  □ 是否启用了复杂的 DevTools 扩展? → 生产环境移除

2. 应用越用越卡

通常是内存泄漏。排查步骤:
  1. 打开 DevTools → Memory → 拍摄 Heap Snapshot
  2. 正常使用应用 5 分钟
  3. 再拍一次 Heap Snapshot
  4. 对比两次快照,按 "Retained Size" 排序
  5. 找到增长最大的对象类型

  常见原因:
  • 事件监听器累积(检查 MaxListenersExceededWarning)
  • WebContents 未销毁
  • 全局缓存无上限增长 → 加 LRU 策略

3. IPC 通信感觉慢

IPC 性能参考:
  单次 invoke 往返: ~0.1-0.5ms(小数据)
  传输 1MB 数据:    ~5-10ms(需要序列化)
  传输 100MB 数据:  ~500ms+(应该避免)

优化方向:
  • 批量化请求(减少往返次数)
  • 减小传输数据体积(只传必要字段)
  • 大文件传路径而非内容(让对方进程自行读取)
  • 使用 MessagePort 建立直连通道(避免主进程中转)

4. 渲染进程内存持续增长

渲染进程是 Chromium 页面,Web 端的内存问题同样适用:
  • 未解绑的事件监听(addEventListener 无对应 remove)
  • 游离 DOM 节点(JS 仍引用已移除的 DOM 元素)
  • 闭包捕获大作用域
  • console.log 保留对象引用(生产环境应移除)

实践建议

┌──────────────────────────────────────────────────────────────┐
│              Electron 性能优化最佳实践                        │
│                                                              │
│  ── 启动优化 ──                                              │
│  1. 延迟加载非必要模块(最重要的一条!)                      │
│     只在 main.js 顶部 require 创建窗口所必需的模块            │
│                                                              │
│  2. 使用 show: false + ready-to-show                         │
│     消除白屏闪烁,提升感知速度                                │
│                                                              │
│  3. 渲染进程代码做代码分割                                    │
│     首屏只加载必要的 JS,其余懒加载                           │
│                                                              │
│  ── 运行时优化 ──                                            │
│  4. 永远不要阻塞主进程                                       │
│     CPU 密集任务 → Worker Thread 或 Utility Process           │
│     大文件操作 → 流式处理                                     │
│                                                              │
│  5. IPC 通信批量化                                           │
│     合并多次小请求为一次批量请求                              │
│     大数据传路径,不传内容                                    │
│                                                              │
│  6. 渲染进程遵循 Web 性能最佳实践                             │
│     虚拟滚动、防抖节流、requestIdleCallback                   │
│                                                              │
│  ── 内存管理 ──                                              │
│  7. 窗口关闭时置空引用                                       │
│     win.on('closed', () => { win = null; })                  │
│                                                              │
│  8. 事件监听成对出现                                          │
│     on ↔ removeListener,确保生命周期对称                     │
│                                                              │
│  ── 度量与监控 ──                                            │
│  9. 建立性能基线                                              │
│     记录启动时间、内存占用,作为优化参照                      │
│                                                              │
│ 10. CI 性能回归检测                                           │
│     在 CI 中运行性能测试,启动时间超阈值则报警                 │
│                                                              │
│  Electron 官方的 8 条建议总结:                                │
│  ① 谨慎引入模块  ② 不要过早加载代码  ③ 不要阻塞主进程        │
│  ④ 不要阻塞渲染进程  ⑤ 不要用 polyfill  ⑥ 不要发无必要的网络请求│
│  ⑦ 不要打包不需要的资源  ⑧ 使用正确的工具分析性能              │
└──────────────────────────────────────────────────────────────┘

下一章:我们将学习如何为 Electron 应用进行代码签名、公证和生产发布 →