调试与性能优化

深入理解 Electron 应用的调试技巧、主进程和渲染进程的 DevTools 使用、以及常见性能问题的排查与优化方法。


Electron 调试工具

开启 DevTools

渲染进程 DevTools

// main.js - 随时打开 DevTools
mainWindow.webContents.openDevTools();

// 或者在特定条件时开启
if (process.env.NODE_ENV === 'development') {
  mainWindow.webContents.openDevTools();
}

主进程调试

主进程使用 Node.js 调试器:

# 启动时开启调试端口
electron --inspect=5858 .

# 或者调试并暂停
electron --inspect-brk .

然后在 VS Code 或 Chrome DevTools 中连接:

# Chrome DevTools 连接
chrome://inspect

VS Code 调试配置

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Main Process",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "runtimeArgs": ["."],
      "env": {
        "NODE_ENV": "development"
      },
      "preLaunchTask": "${defaultBuildTask}"
    },
    {
      "type": "chrome",
      "request": "launch",
      "name": "Debug Renderer Process",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}"
    }
  ]
}

主进程调试

使用 electron-log

const log = require('electron-log');

// 配置日志
log.transports.file.level = 'debug';
log.transports.file.maxSize = 5 * 1024 * 1024;  // 5MB
log.transports.console.level = 'debug';

// 格式化
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}';

// 主进程日志
log.info('Application starting...');
log.warn('Deprecated API used');
log.error('Failed to load module', { error: err.message });

远程调试主进程

// 在主进程中开启调试服务器
const { app } = require('electron');

if (process.env.NODE_ENV === 'development') {
  app.commandLine.appendSwitch('inspect', '5858');
  app.commandLine.appendSwitch('inspect-brk', '5858');
}

常见主进程错误处理

process.on('uncaughtException', (error) => {
  log.error('Uncaught Exception:', error);
  dialog.showErrorBox('Error', `An unexpected error occurred: ${error.message}`);
  app.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  log.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

渲染进程调试

Webpack 源码映射

// webpack.main.config.js
module.exports = {
  // ...
  devtool: 'source-map'  // 或 'eval-source-map'
};

React DevTools

安装 React DevTools 扩展:

// main.js - 加载 React DevTools
const path = require('path');

BrowserWindow.addDevToolsExtensions(
  path.join(__dirname, 'react-devtools-extensions')
);

Redux DevTools(如果是 Redux 应用)

// preload.js - 如果需要 Redux DevTools
contextBridge.exposeInMainWorld('__REDUX_DEVTOOLS_EXTENSION__', {
  connect: () => {
    // Redux DevTools 连接逻辑
  }
});

内存泄漏排查

常见内存泄漏场景

1. 事件监听未移除

// 错误:事件监听重复注册
function onClickHandler() {
  document.addEventListener('click', handleClick);  // 每次调用都注册
}

// 正确:在组件卸载时移除
class MyComponent {
  constructor() {
    this.handleClick = this.handleClick.bind(this);
    document.addEventListener('click', this.handleClick);
  }

  destroy() {
    document.removeEventListener('click', this.handleClick);
  }
}

2. 定时器未清除

// 错误
setInterval(() => {
  updateData();
}, 1000);

// 正确:保存引用,需要时清除
this.intervalId = setInterval(() => {
  updateData();
}, 1000);

// 在组件销毁时
clearInterval(this.intervalId);

3. 闭包引用

// 错误:闭包持有大量数据
function createHandler() {
  const largeData = loadLargeData();  // 占用大量内存

  return () => {
    console.log('Handler executed');
    // largeData 永远不会被释放
  };
}

// 正确:使用 WeakMap 或及时清理
const handlerWeakMap = new WeakMap();

function createHandler() {
  const largeData = loadLargeData();

  const handler = () => {
    console.log('Handler executed');
  };

  handlerWeakMap.set(handler, largeData);

  return handler;
}

内存分析工具

使用 Chrome DevTools Memory 面板:

  1. Heap Snapshot:拍摄内存快照,比较对象
  2. Allocation Timeline:记录内存分配时间线
  3. Sampling Profile:采样分析内存使用
// 在代码中添加标记,便于在 DevTools 中识别
console.memory  // 查看当前内存使用

CPU 性能问题

主线程阻塞

// 错误:在主线程执行耗时操作
ipcMain.handle('process-data', async (event, hugeArray) => {
  return hugeArray.reduce((acc, item) => {
    // 同步处理大量数据,会阻塞 UI
    return acc + processItem(item);
  }, 0);
});

// 正确:使用 Worker 或分片处理
ipcMain.handle('process-data', async (event, hugeArray) => {
  return new Promise((resolve) => {
    // 分批处理
    const batchSize = 1000;
    let index = 0;

    function processBatch() {
      const batch = hugeArray.slice(index, index + batchSize);
      const result = batch.reduce((acc, item) => acc + processItem(item), 0);

      index += batchSize;

      if (index < hugeArray.length) {
        // 让出主线程
        setImmediate(processBatch);
      } else {
        resolve(result);
      }
    }

    processBatch();
  });
});

Web Worker 使用

// worker.js
self.onmessage = function(e) {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

// main.js 或 renderer.js
const worker = new Worker('worker.js');

worker.postMessage(hugeData);
worker.onmessage = function(e) {
  console.log('Result:', e.data);
};

渲染性能优化

减少重排和重绘

// 错误:多次触发重排
function updateList(items) {
  const container = document.getElementById('list');
  container.innerHTML = '';

  items.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item.name;
    container.appendChild(div);  // 每次 append 触发重排
  });
}

// 正确:使用 DocumentFragment 或一次性更新
function updateList(items) {
  const container = document.getElementById('list');
  const fragment = document.createDocumentFragment();

  items.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item.name;
    fragment.appendChild(div);
  });

  container.innerHTML = '';
  container.appendChild(fragment);  // 只触发一次重排
}

// 或者使用 display: none 隐藏后更新
container.style.display = 'none';
container.innerHTML = newHTML;
container.style.display = '';

图片优化

// 使用懒加载
const lazyImage = new Image();
lazyImage.src = 'large-image.jpg';
lazyImage.onload = () => {
  imageElement.src = lazyImage.src;
};

// 使用 srcset
// <img srcset="small.jpg 480w, large.jpg 1080w" sizes="(max-width: 600px) 480px, 1080px">

虚拟列表

对于大量数据,使用虚拟滚动:

// react-window 或 react-virtualized
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  return (
    <FixedSizeList
      height={400}
      itemCount={items.length}
      itemSize={50}
      width={300}
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

启动性能优化

延迟加载

// main.js
app.whenReady().then(() => {
  // 只加载必要的模块
  require('./main-window');

  // 延迟加载其他功能
  setTimeout(() => {
    require('./background-tasks');
    require('./tray');
  }, 5000);
});

预加载优化

// preload.js 应该尽量轻量
// 只暴露必要的 API,避免在 preload 中做重计算

// preload.js - 轻量化
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // 只暴露必要的方法
  readFile: (path) => ipcRenderer.invoke('file:read', path)
  // 不要在这里添加复杂逻辑
});

窗口显示优化

const mainWindow = new BrowserWindow({
  show: false,  // 先不显示
  backgroundColor: '#ffffff'
});

mainWindow.once('ready-to-show', () => {
  mainWindow.show();  // 渲染完成后再显示
});

IPC 性能

避免频繁 IPC 调用

// 错误:每次输入都 IPC
input.addEventListener('input', (e) => {
  ipcRenderer.invoke('search', e.target.value);  // 频繁调用
});

// 正确:使用防抖
import { debounce } from 'lodash';

const debouncedSearch = debounce((query) => {
  ipcRenderer.invoke('search', query);
}, 300);

input.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

批量 IPC

// 错误:多次小请求
async function loadData() {
  const user = await ipcRenderer.invoke('get-user');
  const posts = await ipcRenderer.invoke('get-posts');
  const settings = await ipcRenderer.invoke('get-settings');
}

// 正确:合并为一次请求
ipcMain.handle('get-all-data', async () => {
  return {
    user: await getUser(),
    posts: await getPosts(),
    settings: await getSettings()
  };
});

async function loadData() {
  const data = await ipcRenderer.invoke('get-all-data');
}

这一章想说的

Electron 调试与性能优化:

  1. 调试工具:主进程调试、渲染进程 DevTools、VS Code 配置
  2. 内存泄漏:事件监听、定时器、闭包是常见原因
  3. CPU 问题:主线程阻塞时使用 Worker 或分片处理
  4. 渲染优化:减少重排重绘、虚拟列表、图片懒加载
  5. IPC 优化:避免频繁调用、使用防抖、批量请求

性能问题需要用工具定位,DevTools 是最重要的调试武器。


延展阅读