Electron 作为跨平台桌面应用开发框架,提供了强大的能力,但在启动速度和内存占用方面常被诟病。本文档系统性地介绍 Electron 应用的性能优化策略,覆盖启动速度、内存管理、运行时性能等核心领域。
Electron 采用多进程架构,由一个主进程(Main Process)和多个渲染进程(Renderer Process)组成:
┌─────────────────────────────────────────────────────────────┐
│ Main Process │
│ - 管理 App 生命周期 │
│ - 创建 BrowserWindow │
│ - 原生 API 调用 │
│ - Node.js 环境 │
└─────────────────────────────────────────────────────────────┘
│ IPC (Inter-Process Communication)
▼
┌─────────────────────────────────────────────────────────────┐
│ Renderer Processes │
│ - 渲染 Web 页面 │
│ - Chromium 环境 │
│ - DOM / CSS / JavaScript │
│ - 每个窗口独立进程 │
└─────────────────────────────────────────────────────────────┘| 特性 | 优势 | 挑战 |
|---|---|---|
| 跨平台 | 一套代码多端运行 | Chromium 体积大 |
| Web 技术 | 前端开发友好 | JS 执行效率限制 |
| Node.js 集成 | 原生能力强大 | 内存占用较高 |
| 多进程架构 | 安全隔离性好 | IPC 通信开销 |
Electron 应用启动涉及主进程和渲染进程的双重初始化:
App Launch
│
├── Node.js 环境初始化 ─┐
│ └── 模块加载与解析 │ 主进程
│ │ (同步阻塞)
├── Electron 主进程初始化 ─┘
│
├── BrowserWindow 创建 ─┐
│ └── Chromium 环境初始化 │ 渲染进程
│ │
├── 页面加载与渲染 ─┘
│ ├── HTML 解析
│ ├── CSS 解析
│ ├── JavaScript 执行
│ └── 首帧渲染
│
└── 应用就绪 (TTI)关键瓶颈点:
require/import 的同步执行| 瓶颈类型 | 表现 | 原因 |
|---|---|---|
| 长任务阻塞 | UI 卡顿、响应延迟 | 主线程执行耗时 JS |
| 频繁重排重绘 | 动画掉帧 | DOM 操作触发 Layout |
| 内存泄漏 | 内存持续增长 | 事件监听器未清理 |
| 大数据渲染 | 滚动卡顿 | DOM 节点过多 |
// ❌ 同步 IPC - 阻塞渲染进程
const result = ipcRenderer.sendSync('get-data')
// ✅ 异步 IPC - 不阻塞
const result = await ipcRenderer.invoke('get-data')| 指标 | 英文名称 | 描述 | 目标值 |
|---|---|---|---|
| 启动就绪时间 | Time to Ready | app.ready 事件触发时间 | < 1s |
| 首次绘制时间 | First Paint (FP) | 首像素渲染时间 | < 1.5s |
| 可交互时间 | Time to Interactive (TTI) | 用户可交互时间 | < 3s |
| 帧率 | FPS | 动画流畅度 | ≥ 55 FPS |
| CPU 占用 | CPU Usage | 空闲状态 CPU | < 5% |
| 内存占用 | Memory Usage | 空闲状态内存 | < 200MB |
// 主进程启动时间
console.time('app-ready')
app.whenReady().then(() => {
console.timeEnd('app-ready')
})
// 渲染进程性能
const timing = performance.getEntriesByType('navigation')[0]
console.log({
domContentLoaded: timing.domContentLoadedEventEnd - timing.startTime,
load: timing.loadEventEnd - timing.startTime
})
// 实时 FPS 监控
let lastTime = performance.now()
let frames = 0
function measureFPS() {
frames++
const now = performance.now()
if (now - lastTime >= 1000) {
console.log(`FPS: ${frames}`)
frames = 0
lastTime = now
}
requestAnimationFrame(measureFPS)
}详细内容参见「启动优化」章节。
| 策略 | 效果 | 难度 |
|---|---|---|
| 延迟加载模块 | ⭐⭐⭐ | 低 |
| 代码分割 | ⭐⭐⭐ | 中 |
| V8 快照 | ⭐⭐ | 高 |
| 窗口预热 | ⭐⭐ | 中 |
| 骨架屏 | ⭐⭐ | 低 |
详细内容参见「内存管理」章节。
| 策略 | 效果 | 难度 |
|---|---|---|
| 虚拟列表 | ⭐⭐⭐ | 中 |
| 事件监听器清理 | ⭐⭐⭐ | 低 |
| 窗口复用 | ⭐⭐ | 中 |
| 缓存管理 | ⭐⭐ | 低 |
| 懒加载 | ⭐⭐ | 低 |
// 1. 虚拟滚动 - 大列表优化
import { FixedSizeList } from 'react-window'
<FixedSizeList
height={600}
itemCount={10000}
itemSize={35}
>
{({ index, style }) => <div style={style}>{items[index]}</div>}
</FixedSizeList>
// 2. requestAnimationFrame - 动画优化
function animate() {
// 动画逻辑
requestAnimationFrame(animate)
}
// 3. Web Worker - 耗时任务
const worker = new Worker('compute.js')
worker.postMessage({ data: heavyData })
worker.onmessage = (e) => handleResult(e.data)
// 4. CSS 优化
.optimized-element {
will-change: transform;
transform: translateZ(0);
}Electron 内置 Chromium,可直接使用 Chrome DevTools:
// 自动打开 DevTools
win.webContents.openDevTools()
// 性能分析
// 1. Performance 面板 - 录制运行时性能
// 2. Memory 面板 - 堆快照分析内存泄漏
// 3. Layers 面板 - 查看渲染层结构import { app } from 'electron'
// 获取进程指标
const metrics = app.getAppMetrics()
metrics.forEach(m => {
console.log(`PID: ${m.pid}, Memory: ${m.memory.workingSetSize}KB`)
})
// GPU 信息
const gpuInfo = await app.getGPUInfo('complete')
console.log(gpuInfo.auxAttributes)
// 进程内存
const usage = process.memoryUsage()
console.log({
rss: `${(usage.rss / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(2)} MB`
})| 工具 | 用途 | 安装 |
|---|---|---|
| clinic.js | 性能诊断 | npm i -g clinic |
| electron-profiler | CPU 分析 | npm i electron-profiler |
注:早期流行的 Devtron 检查器已停止维护(官方文档页已下线),其功能可由 Chrome DevTools 与
app.getAppMetrics()替代。
ready-to-show 事件显示窗口sendSync)transform 和 opacityElectron 应用的启动速度直接影响用户的第一印象。本文档深入分析启动流程,提供系统化的优化策略。
App Launch
│
├─ [1] Node.js 环境初始化
│ ├── V8 引擎启动
│ ├── Node.js 绑定加载
│ └── 主进程入口执行
│
├─ [2] Electron 主进程初始化
│ ├── App 模块就绪
│ ├── 模块加载 (require)
│ └── 业务初始化
│
├─ [3] BrowserWindow 创建
│ ├── 渲染进程创建
│ ├── Chromium 初始化
│ └── Preload 脚本执行
│
├─ [4] 页面加载
│ ├── HTML 解析
│ ├── CSS 解析
│ ├── JavaScript 执行
│ └── 资源加载
│
├─ [5] 首次渲染
│ ├── DOM 树构建
│ ├── Render 树构建
│ └── 首帧绘制
│
└─ [6] 应用就绪 (TTI)| 阶段 | 典型耗时 | 优化潜力 | 优化难度 |
|---|---|---|---|
| Node.js 初始化 | 50-100ms | 低 | - |
| 模块加载 | 100-500ms | 高 | 低 |
| BrowserWindow 创建 | 100-300ms | 中 | 中 |
| 页面加载解析 | 200-1000ms | 高 | 中 |
| 首帧渲染 | 50-200ms | 中 | 中 |
// main.ts - 主进程性能追踪
import { app } from 'electron'
const startTime = Date.now()
// 阶段 1:进程启动
console.log(`[Perf] Process start: ${startTime}`)
// 阶段 2:App Ready
app.whenReady().then(() => {
console.log(`[Perf] App ready: ${Date.now() - startTime}ms`)
})
// 阶段 3:窗口创建
const win = new BrowserWindow({ /* ... */ })
console.log(`[Perf] Window created: ${Date.now() - startTime}ms`)
// 阶段 4:页面加载
win.webContents.on('did-finish-load', () => {
console.log(`[Perf] Page loaded: ${Date.now() - startTime}ms`)
})
// 阶段 5:首次渲染(ready-to-show 表示首帧已绘制完成)
win.once('ready-to-show', () => {
console.log(`[Perf] First render: ${Date.now() - startTime}ms`)
})// renderer.ts - 渲染进程性能追踪
const paintObserver = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
console.log(`[Perf] ${entry.name}: ${entry.startTime}ms`)
})
})
paintObserver.observe({ entryTypes: ['paint', 'navigation'] })
// 关键指标
window.addEventListener('load', () => {
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
console.table({
'DNS Lookup': timing.domainLookupEnd - timing.domainLookupStart,
'TCP Connect': timing.connectEnd - timing.connectStart,
'Request': timing.responseStart - timing.requestStart,
'Response': timing.responseEnd - timing.responseStart,
'DOM Parse': timing.domInteractive - timing.responseEnd,
'DOM Content Loaded': timing.domContentLoadedEventEnd - timing.startTime,
'Load': timing.loadEventEnd - timing.startTime
})
})问题:在主进程顶部导入所有依赖会导致启动时同步加载所有模块。
// ❌ 启动时加载所有模块
import { dialog } from 'electron'
import { autoUpdater } from 'electron-updater'
import Store from 'electron-store'
import log from 'electron-log'
import fs from 'fs'
import path from 'path'
// ✅ 延迟加载非必要模块
// 方式一:函数内加载
function getDialog() {
return require('electron').dialog
}
// 方式二:惰性初始化
let _store: Store | null = null
function getStore() {
if (!_store) {
_store = new Store()
}
return _store
}
// 方式三:动态导入 (ESM)
async function openSettings() {
const { SettingsWindow } = await import('./windows/settings')
return SettingsWindow.show()
}效果对比:
| 场景 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 10 个模块加载 | 320ms | 85ms | 73% |
// main.ts
import { app, BrowserWindow } from 'electron'
async function bootstrap() {
// 必须同步:创建主窗口
const win = createMainWindow()
// 可异步:非关键初始化
Promise.all([
initAutoUpdater(), // 自动更新检查
initTray(), // 系统托盘
initGlobalShortcuts(), // 全局快捷键
initIPC(), // IPC 处理器
loadUserConfig() // 用户配置
]).catch(err => {
console.error('Background init failed:', err)
})
return win
}
app.whenReady().then(bootstrap)
// 单独的初始化函数
async function initAutoUpdater() {
const { autoUpdater } = await import('electron-updater')
autoUpdater.checkForUpdatesAndNotify()
}
async function initTray() {
const { Tray } = await import('electron')
// ... 托盘初始化
}// 根据功能开关决定是否加载
const features = loadFeatureFlags()
if (features.analytics) {
import('./analytics').then(m => m.init())
}
if (features.crashReport) {
import('./crash-reporter').then(m => m.init())
}
// 根据平台决定
if (process.platform === 'darwin') {
import('./mac-specific').then(m => m.init())
}避免显示空白窗口,等待页面准备就绪后再显示:
import { BrowserWindow } from 'electron'
const win = new BrowserWindow({
show: false, // 初始隐藏
backgroundColor: '#1e1e1e', // 背景色匹配应用主题
// ... 其他配置
})
win.loadFile('index.html')
// 等待页面准备就绪
win.once('ready-to-show', () => {
win.show() // 显示窗口
console.log('[Perf] Window shown')
})预创建可能使用的窗口:
class WindowManager {
private windows = new Map<string, BrowserWindow>()
private preloadQueue: string[] = []
// 创建主窗口时预热其他窗口
createMainWindow() {
const mainWin = new BrowserWindow({ /* ... */ })
mainWin.loadFile('index.html')
// 主窗口加载完成后预热
mainWin.webContents.on('did-finish-load', () => {
this.preloadWindows(['settings', 'about'])
})
return mainWin
}
// 预加载窗口列表
private preloadWindows(names: string[]) {
names.forEach(name => {
if (!this.windows.has(name)) {
const win = new BrowserWindow({
show: false,
backgroundColor: '#1e1e1e',
webPreferences: {
preload: path.join(__dirname, `${name}.preload.js`)
}
})
win.loadFile(`${name}.html`)
this.windows.set(name, win)
}
})
}
// 获取窗口(预热或创建)
getWindow(name: string): BrowserWindow {
if (this.windows.has(name)) {
const win = this.windows.get(name)!
win.show()
return win
}
// 懒创建
return this.createWindow(name)
}
}适用于频繁创建销毁的同类型窗口:
interface PoolItem {
win: BrowserWindow
inUse: boolean
id: string
}
class WindowPool {
private pool: PoolItem[] = []
private maxSize = 5
constructor(private config: BrowserWindowConstructorOptions) {
// 初始化时预创建
this.init()
}
private init() {
for (let i = 0; i < 2; i++) {
this.createWindow()
}
}
private createWindow(): PoolItem {
const win = new BrowserWindow({
...this.config,
show: false
})
const item: PoolItem = {
win,
inUse: false,
id: `pool-${Date.now()}-${Math.random()}`
}
this.pool.push(item)
return item
}
acquire(): BrowserWindow {
// 查找空闲窗口
let item = this.pool.find(i => !i.inUse)
if (!item && this.pool.length < this.maxSize) {
item = this.createWindow()
}
if (!item) {
throw new Error('Window pool exhausted')
}
item.inUse = true
item.win.show()
return item.win
}
release(win: BrowserWindow) {
const item = this.pool.find(i => i.win === win)
if (item) {
item.inUse = false
item.win.hide()
// 重置窗口状态
item.win.loadFile('blank.html')
}
}
destroy() {
this.pool.forEach(item => {
if (!item.win.isDestroyed()) {
item.win.close()
}
})
this.pool = []
}
}
// 使用示例
const detailWindowPool = new WindowPool({
width: 800,
height: 600,
webPreferences: { nodeIntegration: false }
})
function openDetail(id: string) {
const win = detailWindowPool.acquire()
win.loadFile('detail.html', { query: { id } })
win.once('closed', () => {
detailWindowPool.release(win)
})
}const win = new BrowserWindow({
// 减少不必要的功能
autoHideMenuBar: true, // 隐藏菜单栏
frame: true, // 保留原生边框(更快)
transparent: false, // 避免透明窗口开销
// 优化渲染
backgroundColor: '#fff', // 设置背景色避免闪烁
webPreferences: {
nodeIntegration: false, // 安全 + 性能
contextIsolation: true,
spellcheck: false, // 禁用拼写检查(如不需要)
devTools: isDev // 仅开发环境开启
}
})// Vite/Webpack 动态导入
const routes = [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/settings',
component: () => import('./views/Settings.vue')
},
{
path: '/editor',
component: () => import('./views/Editor.vue')
}
]
// Vue 组件懒加载
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
// 带加载状态的异步组件
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
timeout: 3000
})<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- 关键 CSS 内联 -->
<style>
body { margin: 0; background: #1e1e1e; }
.skeleton { /* 骨架屏样式 */ }
</style>
<!-- 预加载关键资源 -->
<link rel="preload" href="critical.js" as="script">
<!-- 非关键 CSS 延迟加载 -->
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
</head>
<body>
<!-- 骨架屏 -->
<div id="app">
<div class="skeleton">
<div class="skeleton-header"></div>
<div class="skeleton-content"></div>
</div>
</div>
<!-- 关键 JS -->
<script src="critical.js"></script>
<!-- 非关键 JS 延迟执行 -->
<script defer src="analytics.js"></script>
<script defer src="non-critical.js"></script>
</body>
</html><!-- Skeleton.vue -->
<template>
<div class="skeleton-container">
<div class="skeleton-header">
<div class="skeleton-avatar"></div>
<div class="skeleton-title"></div>
</div>
<div class="skeleton-body">
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
<div class="skeleton-line"></div>
</div>
</div>
</template>
<style scoped>
.skeleton-container {
padding: 20px;
}
.skeleton-avatar,
.skeleton-title,
.skeleton-line {
background: linear-gradient(
90deg,
#2a2a2a 25%,
#3a3a3a 50%,
#2a2a2a 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>// main.ts - 主进程预加载数据
let mainWindow: BrowserWindow
app.whenReady().then(async () => {
// 预加载缓存数据
const cachedData = await loadCachedData()
mainWindow = createWindow()
mainWindow.loadFile('index.html')
// 窗口就绪后发送预加载数据
mainWindow.webContents.on('did-finish-load', () => {
mainWindow.webContents.send('preload:data', cachedData)
})
})
// renderer.ts - 渲染进程使用预加载数据
let preloadedData: any = null
ipcRenderer.on('preload:data', (_, data) => {
preloadedData = data
renderWithCache(data)
})
async function renderWithCache(cache: any) {
if (cache) {
// 立即使用缓存渲染
updateUI(cache)
}
// 后台获取最新数据
const freshData = await fetchLatestData()
updateUI(freshData)
}缓存 V8 编译结果,加速模块加载:
npm install v8-compile-cache// main.ts - 必须在其他 require 之前
import 'v8-compile-cache'
// 或者
require('v8-compile-cache')
// 后续的 require 会自动使用缓存
import { app, BrowserWindow } from 'electron'效果:
| 模块数量 | 无缓存 | 有缓存 | 提升 |
|---|---|---|---|
| 50 个 | 450ms | 180ms | 60% |
| 100 个 | 820ms | 310ms | 62% |
预编译模块到 V8 快照:
// 1. 创建 snapshot.js - 列出需要快照的模块
// snapshot.js
require('lodash')
require('moment')
require('axios')
// 2. 生成快照脚本
import electronLink from 'electron-link'
import { execFileSync } from 'child_process'
async function generateSnapshot() {
const result = await electronLink({
baseDirPath: process.cwd(),
mainPath: `${process.cwd()}/snapshot.js`,
cachePath: `${process.cwd()}/cache`
})
// 生成快照文件
execFileSync('mksnapshot', [
'cache/snapshot.js',
'--output_dir', process.cwd()
])
}
// 3. 加载快照
if (typeof snapshotResult !== 'undefined') {
const Module = require('module')
const originalLoad = Module._load
Module._load = function(module: string) {
const cached = snapshotResult.customRequire.cache[module]
if (cached) return cached.exports
return originalLoad.apply(this, arguments as any)
}
}⚠️ 注意:V8 快照不能用于有副作用的代码(如 I/O 操作、DOM 操作)。
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
mode: process.env.NODE_ENV,
build: {
target: 'esnext', // Electron 支持最新 ES 特性
minify: 'terser',
terserOptions: {
compress: {
drop_console: !isDev,
drop_debugger: !isDev
}
},
rollupOptions: {
output: {
manualChunks: {
// 分离第三方库
vendor: ['vue', 'vue-router', 'pinia'],
electron: ['electron'],
utils: ['lodash-es', 'dayjs']
}
}
},
// 启用 gzip 压缩报告
reportCompressedSize: true
},
// 优化依赖预构建
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia']
}
})// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
minimize: true,
usedExports: true, // Tree Shaking
sideEffects: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
},
externals: {
// 排除大型库,使用 CDN 或本地
// lodash: 'lodash' // 根据实际情况
}
}// 图片压缩脚本
import imagemin from 'imagemin'
import imageminPngquant from 'imagemin-pngquant'
import imageminMozjpeg from 'imagemin-mozjpeg'
await imagemin(['src/images/*.{jpg,png}'], {
destination: 'dist/images',
plugins: [
imageminMozjpeg({ quality: 80 }),
imageminPngquant({ quality: [0.6, 0.8] })
]
})// perf-collector.ts
interface PerfMetrics {
processStart: number
appReady: number
windowCreated: number
pageLoaded: number
firstPaint: number
interactive: number
}
class PerformanceCollector {
private metrics: Partial<PerfMetrics> = {}
private startTime = Date.now()
mark(key: keyof PerfMetrics) {
this.metrics[key] = Date.now() - this.startTime
}
report() {
console.table({
'Process Start': `${this.metrics.processStart}ms`,
'App Ready': `${this.metrics.appReady}ms`,
'Window Created': `${this.metrics.windowCreated}ms`,
'Page Loaded': `${this.metrics.pageLoaded}ms`,
'First Paint': `${this.metrics.firstPaint}ms`,
'Interactive': `${this.metrics.interactive}ms`,
'Total TTI': `${this.metrics.interactive}ms`
})
// 上报到监控系统
if (process.env.REPORT_PERF) {
fetch('/api/perf', {
method: 'POST',
body: JSON.stringify(this.metrics)
})
}
}
}
export const perfCollector = new PerformanceCollector()// renderer.ts
const perfObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// 上报长任务
if (entry.duration > 50) {
console.warn('Long task detected:', {
name: entry.name,
duration: entry.duration,
startTime: entry.startTime
})
}
}
})
perfObserver.observe({ entryTypes: ['longtask', 'paint', 'navigation'] })背景:某代码编辑器启动耗时 5 秒,用户体验差。
问题分析:
阶段 耗时 占比
─────────────────────────────
模块加载 1800ms 36%
窗口创建 800ms 16%
Monaco Editor 1200ms 24%
插件初始化 700ms 14%
首帧渲染 500ms 10%
─────────────────────────────
总计 5000ms 100%优化措施:
// 仅在需要时加载编辑器
const editorContainer = document.getElementById('editor')
editorContainer.addEventListener('click', async () => {
const monaco = await import('monaco-editor')
monaco.editor.create(editorContainer, { /* config */ })
}, { once: true })// 核心插件立即初始化,其他延迟
const corePlugins = ['file-manager', 'search']
const allPlugins = await loadPluginList()
// 先加载核心插件
await Promise.all(corePlugins.map(p => loadPlugin(p)))
// 其他插件后台加载
setTimeout(() => {
allPlugins
.filter(p => !corePlugins.includes(p.id))
.forEach(p => loadPlugin(p))
}, 2000)// 对高频使用的模块生成 V8 快照
require('v8-compile-cache')优化结果:
阶段 优化前 优化后 提升
───────────────────────────────────────
模块加载 1800ms 450ms 75%
窗口创建 800ms 600ms 25%
Monaco Editor 1200ms 0ms 100% (延迟)
插件初始化 700ms 200ms 71%
首帧渲染 500ms 400ms 20%
───────────────────────────────────────
总计 5000ms 1650ms 67%
TTI 5000ms 1650ms 67%| 优化项 | 效果 | 难度 | 优先级 |
|---|---|---|---|
| 延迟加载模块 | ⭐⭐⭐ | 低 | P0 |
| ready-to-show | ⭐⭐ | 低 | P0 |
| 骨架屏 | ⭐⭐ | 低 | P1 |
| 代码分割 | ⭐⭐⭐ | 中 | P1 |
| v8-compile-cache | ⭐⭐ | 低 | P1 |
| 异步初始化 | ⭐⭐ | 低 | P1 |
| 窗口预热 | ⭐⭐ | 中 | P2 |
| V8 快照 | ⭐⭐ | 高 | P2 |
| 窗口池 | ⭐⭐ | 中 | P3 |
使用 ready-to-show 事件和背景色:
const win = new BrowserWindow({
show: false,
backgroundColor: '#1e1e1e' // 匹配应用主题
})
win.once('ready-to-show', () => {
win.show()
})使用性能分析:
// 在每个 require 前后打印时间
const originalRequire = Module.prototype.require
Module.prototype.require = function(id: string) {
const start = Date.now()
const result = originalRequire.apply(this, arguments as any)
console.log(`[Module] ${id}: ${Date.now() - start}ms`)
return result
}或使用 Chrome DevTools 的 Performance 面板录制启动过程。
采用预加载策略:
// 用户可能使用的功能提前加载
win.webContents.on('did-finish-load', () => {
// 空闲时预加载
requestIdleCallback(() => {
import('./features/settings')
import('./features/search')
})
})const isDev = process.env.NODE_ENV === 'development'
// 开发环境禁用某些优化以加快热更新
if (!isDev) {
require('v8-compile-cache')
}
// 生产环境启用压缩
const config = {
minify: !isDev,
sourcemap: isDev
}Electron 应用基于 Chromium,内存占用通常较大。本文档深入分析内存结构、泄漏诊断和优化策略。
┌─────────────────────────────────────────────────────────────┐
│ Main Process │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Node.js 堆内存 │ │
│ │ - V8 Heap (默认 ~1.4GB 限制) │ │
│ │ - Buffer 空间 │ │
│ │ - Native Addons │ │
│ └─────────────────────────────────────────────────────┘ │
│ 常驻内存: 50-200 MB │
└─────────────────────────────────────────────────────────────┘
│
│ IPC
▼
┌─────────────────────────────────────────────────────────────┐
│ Renderer Process │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ V8 堆内存 │ │
│ │ - JavaScript 对象 │ │
│ │ - 闭包 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DOM 内存 │ │
│ │ - DOM 树 │ │
│ │ - CSSOM │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GPU 内存 │ │
│ │ - 合成层 │ │
│ │ - 纹理 │ │
│ └─────────────────────────────────────────────────────┘ │
│ 常驻内存: 100-500 MB (视页面复杂度) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ GPU Process │
│ - 图形渲染 │
│ - 视频解码 │
│ 常驻内存: 50-200 MB │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Utility Processes │
│ - 网络服务 │
│ - 存储服务 │
│ - 音频服务 │
└─────────────────────────────────────────────────────────────┘| 指标 | 说明 | 正常范围 |
|---|---|---|
rss | 常驻内存集 | 应用实际占用 |
heapTotal | V8 堆总量 | - |
heapUsed | V8 堆使用量 | < heapTotal 的 70% |
external | 外部内存 (Buffer) | - |
arrayBuffers | ArrayBuffer 内存 | - |
每个 BrowserWindow 创建独立的渲染进程:
窗口数量 基础内存 增量
──────────────────────────
1 150MB -
2 250MB +100MB
3 350MB +100MB
4 450MB +100MB建议:控制同时打开的窗口数量,或采用窗口复用策略。
import { app } from 'electron'
// 定期打印内存使用情况
class MemoryMonitor {
private interval: NodeJS.Timeout | null = null
private history: Array<{ time: number; memory: NodeJS.MemoryUsage }> = []
start(intervalMs = 30000) {
this.interval = setInterval(() => {
const memory = process.memoryUsage()
this.history.push({
time: Date.now(),
memory
})
// 保留最近 100 条记录
if (this.history.length > 100) {
this.history.shift()
}
this.log(memory)
}, intervalMs)
}
stop() {
if (this.interval) {
clearInterval(this.interval)
this.interval = null
}
}
private log(memory: NodeJS.MemoryUsage) {
console.log('[Memory]', {
rss: this.format(memory.rss),
heapTotal: this.format(memory.heapTotal),
heapUsed: this.format(memory.heapUsed),
external: this.format(memory.external),
arrayBuffers: this.format(memory.arrayBuffers)
})
}
private format(bytes: number): string {
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
}
// 检测内存泄漏趋势
detectLeak(): boolean {
if (this.history.length < 10) return false
const recent = this.history.slice(-10)
const oldest = recent[0].memory.heapUsed
const newest = recent[recent.length - 1].memory.heapUsed
// 如果内存增长超过 20%,可能存在泄漏
return (newest - oldest) / oldest > 0.2
}
}
const memoryMonitor = new MemoryMonitor()
app.whenReady().then(() => memoryMonitor.start())// renderer.ts
class RendererMemoryMonitor {
private observer: PerformanceObserver | null = null
start() {
// 监控内存压力
this.observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'measure') {
console.log(`[Perf] ${entry.name}: ${entry.duration}ms`)
}
}
})
this.observer.observe({ entryTypes: ['measure'] })
// 定期检查内存
setInterval(() => {
const memory = (performance as any).memory
if (memory) {
const usedRatio = memory.usedJSHeapSize / memory.totalJSHeapSize
console.log('[Renderer Memory]', {
used: this.format(memory.usedJSHeapSize),
total: this.format(memory.totalJSHeapSize),
limit: this.format(memory.jsHeapSizeLimit),
ratio: `${(usedRatio * 100).toFixed(1)}%`
})
// 内存使用超过 80% 警告
if (usedRatio > 0.8) {
console.warn('[Memory Warning] Heap usage > 80%')
}
}
}, 30000)
}
private format(bytes: number): string {
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
}
}
new RendererMemoryMonitor().start()// main.ts
import { app, BrowserWindow } from 'electron'
function logAllProcessesMemory() {
// 主进程内存
const mainMemory = process.memoryUsage()
console.log('[Main Process]', formatMemory(mainMemory))
// 所有渲染进程
const windows = BrowserWindow.getAllWindows()
windows.forEach(async (win, index) => {
try {
const result = await win.webContents.executeJavaScript(`
JSON.stringify({
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize,
limit: performance.memory.jsHeapSizeLimit
})
`)
const memory = JSON.parse(result)
console.log(`[Renderer ${index}]`, formatMemory(memory))
} catch (e) {
console.error(`[Renderer ${index}] Failed to get memory`)
}
})
}
function formatMemory(memory: any) {
return {
used: `${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
total: `${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`
}
}问题:未清理的事件监听器导致对象无法释放。
// ❌ 内存泄漏:重复添加监听器且不移除
class SearchComponent {
constructor() {
// 每次创建实例都添加监听器
ipcRenderer.on('search-result', this.handleResult)
}
handleResult = (event: any, data: any) => {
// 处理结果
}
// 缺少销毁方法!
}
// ✅ 正确:组件销毁时移除监听器
class SearchComponent {
private boundHandler = this.handleResult.bind(this)
constructor() {
ipcRenderer.on('search-result', this.boundHandler)
}
handleResult(event: any, data: any) {
// 处理结果
}
destroy() {
ipcRenderer.removeListener('search-result', this.boundHandler)
}
}
// ✅ 更好:使用一次性监听器或 AbortController
class SearchComponent {
private abortController = new AbortController()
constructor() {
// 使用 AbortController
ipcRenderer.on('search-result', this.handleResult, {
signal: this.abortController.signal
} as any)
}
destroy() {
this.abortController.abort()
}
}// ❌ 内存泄漏:未清理的定时器
class PollingService {
start() {
setInterval(() => {
this.fetchData()
}, 1000)
}
// 没有 stop 方法!
}
// ✅ 正确:提供清理方法
class PollingService {
private timer: NodeJS.Timeout | null = null
start() {
this.timer = setInterval(() => {
this.fetchData()
}, 1000)
}
stop() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
}
}
// ✅ 更好:使用 FinalizationRegistry 自动清理
class ManagedInterval {
private static registry = new FinalizationRegistry((timer: NodeJS.Timeout) => {
clearInterval(timer)
})
static setInterval(callback: () => void, ms: number, holder: object) {
const timer = setInterval(callback, ms)
this.registry.register(holder, timer)
return timer
}
}
// 使用
const service = new PollingService()
ManagedInterval.setInterval(() => service.fetchData(), 1000, service)
// service 被 GC 时自动清理定时器// ❌ 内存泄漏:闭包持有大对象
class DataProcessor {
private largeData: BigObject
process() {
// 每个回调都持有整个 largeData
return this.largeData.items.map(item => {
return {
...item,
compute: () => this.largeData.config.value + item.value
}
})
}
}
// ✅ 正确:只保留需要的引用
class DataProcessor {
private largeData: BigObject
process() {
// 只保留需要的值
const configValue = this.largeData.config.value
return this.largeData.items.map(item => {
return {
...item,
compute: () => configValue + item.value
}
})
}
}// ❌ 内存泄漏:保留已移除节点的引用
class Modal {
private element: HTMLElement | null = null
show() {
this.element = document.createElement('div')
document.body.appendChild(this.element)
}
hide() {
if (this.element) {
document.body.removeChild(this.element)
// element 变量仍持有引用!
}
}
}
// ✅ 正确:移除后释放引用
class Modal {
private element: HTMLElement | null = null
show() {
this.element = document.createElement('div')
document.body.appendChild(this.element)
}
hide() {
if (this.element) {
document.body.removeChild(this.element)
this.element = null // 释放引用
}
}
}
// ✅ 更好:使用 WeakRef 检测节点是否在 DOM 中
class Modal {
private elementRef: WeakRef<HTMLElement> | null = null
show() {
const element = document.createElement('div')
document.body.appendChild(element)
this.elementRef = new WeakRef(element)
}
hide() {
const element = this.elementRef?.deref()
if (element && element.isConnected) {
element.remove()
}
this.elementRef = null
}
}// ❌ 内存泄漏:缓存无限增长
class DataCache {
private cache = new Map<string, any>()
set(key: string, value: any) {
this.cache.set(key, value)
}
get(key: string) {
return this.cache.get(key)
}
// 没有限制缓存大小!
}
// ✅ 正确:限制缓存大小并使用 LRU 策略
class LRUCache<K, V> {
private cache = new Map<K, V>()
private maxSize: number
constructor(maxSize = 100) {
this.maxSize = maxSize
}
set(key: K, value: V) {
// 如果已存在,先删除(重排序)
if (this.cache.has(key)) {
this.cache.delete(key)
}
// 超出限制,删除最旧的
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
get(key: K): V | undefined {
const value = this.cache.get(key)
if (value !== undefined) {
// 访问后移到末尾(最近使用)
this.cache.delete(key)
this.cache.set(key, value)
}
return value
}
clear() {
this.cache.clear()
}
}
// 使用 WeakMap 存储对象引用
class WeakCache {
private cache = new WeakMap<object, any>()
set(key: object, value: any) {
this.cache.set(key, value)
}
get(key: object) {
return this.cache.get(key)
}
// key 被 GC 时自动清理
}步骤:
1. 打开 DevTools → Memory 面板
2. 选择 "Heap snapshot"
3. 执行以下操作:
- 快照 A:操作前
- 执行可能泄漏的操作(如打开/关闭窗口)
- 快照 B:操作后
4. 选择 "Comparison" 视图对比
5. 关注 "Delta" 为正的对象步骤:
1. 打开 DevTools → Memory 面板
2. 选择 "Allocation instrumentation on timeline"
3. 开始录制
4. 执行操作
5. 停止录制
6. 查看蓝色柱状图(内存分配)
7. 点击柱状图查看分配位置步骤:
1. 打开 DevTools → Memory 面板
2. 选择 "Allocation sampling"
3. 开始采样
4. 执行操作
5. 停止采样
6. 查看 Call Tree 找出分配热点// memory-leak-detector.ts
class MemoryLeakDetector {
private snapshots: Array<{
timestamp: number
heapSize: number
label: string
}> = []
// 记录堆快照
mark(label: string) {
const memory = process.memoryUsage()
this.snapshots.push({
timestamp: Date.now(),
heapSize: memory.heapUsed,
label
})
}
// 分析泄漏趋势
analyze() {
if (this.snapshots.length < 2) {
console.log('Need at least 2 snapshots')
return
}
console.log('\n=== Memory Leak Analysis ===\n')
console.log('Timestamp'.padEnd(20), 'Heap Size'.padEnd(15), 'Label')
console.log('-'.repeat(60))
this.snapshots.forEach((s, i) => {
const prev = this.snapshots[i - 1]
const delta = prev ? s.heapSize - prev.heapSize : 0
const deltaStr = delta >= 0 ? `+${this.format(delta)}` : this.format(delta)
console.log(
new Date(s.timestamp).toISOString().padEnd(20),
this.format(s.heapSize).padEnd(15),
`${s.label} (${deltaStr})`
)
})
// 检测趋势
const first = this.snapshots[0]
const last = this.snapshots[this.snapshots.length - 1]
const growth = last.heapSize - first.heapSize
const growthPercent = (growth / first.heapSize) * 100
console.log('\n--- Summary ---')
console.log(`Total growth: ${this.format(growth)} (${growthPercent.toFixed(1)}%)`)
if (growthPercent > 20) {
console.warn('⚠️ Potential memory leak detected!')
}
}
private format(bytes: number): string {
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
}
}
// 使用示例
const detector = new MemoryLeakDetector()
async function testLeak() {
detector.mark('start')
// 模拟操作
for (let i = 0; i < 10; i++) {
// 执行可能泄漏的操作
await performAction()
detector.mark(`iteration-${i}`)
}
detector.analyze()
}| 泄漏类型 | 堆快照特征 | 排查方法 |
|---|---|---|
| 事件监听器 | Detached EventListener | 检查 addEventListener 调用 |
| DOM 节点 | Detached DOM nodes | 检查 removeChild 后引用 |
| 闭包 | Closure 对象增长 | 检查回调函数引用 |
| 缓存 | Map/Set 持续增长 | 检查缓存清理逻辑 |
| 定时器 | Timeout 对象 | 检查 clearInterval 调用 |
// 窗口复用管理器
class WindowPool {
private windows = new Map<string, BrowserWindow>()
private maxWindows = 5
getOrCreate(name: string, config: BrowserWindowConstructorOptions) {
let win = this.windows.get(name)
if (win && !win.isDestroyed()) {
win.focus()
return win
}
if (this.windows.size >= this.maxWindows) {
// 关闭最早使用的窗口
const firstKey = this.windows.keys().next().value
this.close(firstKey)
}
win = new BrowserWindow(config)
this.windows.set(name, win)
win.on('closed', () => {
this.windows.delete(name)
})
return win
}
close(name: string) {
const win = this.windows.get(name)
if (win) {
win.close()
this.windows.delete(name)
}
}
closeAll() {
this.windows.forEach(win => {
if (!win.isDestroyed()) win.close()
})
this.windows.clear()
}
}<!-- VirtualList.vue -->
<template>
<div class="virtual-list" @scroll="handleScroll" ref="container">
<div class="virtual-list-phantom" :style="{ height: totalHeight + 'px' }"></div>
<div class="virtual-list-content" :style="{ transform: `translateY(${offset}px)` }">
<div
v-for="item in visibleItems"
:key="item.id"
class="virtual-list-item"
:style="{ height: itemHeight + 'px' }"
>
<slot :item="item.data" :index="item.index"></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
const props = defineProps<{
items: any[]
itemHeight: number
bufferSize?: number
}>()
const container = ref<HTMLElement>()
const scrollTop = ref(0)
const bufferSize = props.bufferSize ?? 5
const totalHeight = computed(() => props.items.length * props.itemHeight)
const visibleCount = computed(() => {
if (!container.value) return 20
return Math.ceil(container.value.clientHeight / props.itemHeight) + bufferSize * 2
})
const startIndex = computed(() => {
return Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - bufferSize)
})
const endIndex = computed(() => {
return Math.min(props.items.length, startIndex.value + visibleCount.value)
})
const visibleItems = computed(() => {
return props.items
.slice(startIndex.value, endIndex.value)
.map((data, i) => ({
data,
index: startIndex.value + i,
id: startIndex.value + i
}))
})
const offset = computed(() => startIndex.value * props.itemHeight)
function handleScroll() {
if (container.value) {
scrollTop.value = container.value.scrollTop
}
}
</script>
<style scoped>
.virtual-list {
height: 100%;
overflow-y: auto;
position: relative;
}
.virtual-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.virtual-list-content {
position: absolute;
left: 0;
right: 0;
top: 0;
}
</style><!-- LazyImage.vue -->
<template>
<img
ref="imgRef"
:src="loaded ? src : placeholder"
:alt="alt"
loading="lazy"
@load="onLoad"
/>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps<{
src: string
alt?: string
placeholder?: string
}>()
const imgRef = ref<HTMLImageElement>()
const loaded = ref(false)
let observer: IntersectionObserver | null = null
onMounted(() => {
if (!imgRef.value) return
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loaded.value = true
observer?.disconnect()
}
})
}, {
rootMargin: '100px' // 提前 100px 开始加载
})
observer.observe(imgRef.value)
})
onUnmounted(() => {
observer?.disconnect()
})
function onLoad() {
loaded.value = true
}
</script>// 大数据分页加载
class PaginatedData<T> {
private data: T[] = []
private pageSize: number
private currentPage = 0
private allLoaded = false
constructor(
private fetcher: (page: number, size: number) => Promise<T[]>,
pageSize = 50
) {
this.pageSize = pageSize
}
async loadMore(): Promise<T[]> {
if (this.allLoaded) return []
const items = await this.fetcher(this.currentPage, this.pageSize)
if (items.length < this.pageSize) {
this.allLoaded = true
}
this.data.push(...items)
this.currentPage++
return items
}
get loadedData(): T[] {
return this.data
}
get hasMore(): boolean {
return !this.allLoaded
}
reset() {
this.data = []
this.currentPage = 0
this.allLoaded = false
}
}现象:反复打开关闭图片查看器窗口,内存持续增长。
诊断过程:
Detached HTMLDivElement 数量增长 25 个ImagePreview 组件实例增长 5 个根因分析:
// 问题代码
class ImagePreview {
constructor(private container: HTMLElement) {
// 问题 1:全局事件监听器未清理
window.addEventListener('resize', this.handleResize)
// 问题 2:IPC 监听器未清理
ipcRenderer.on('image-update', this.handleUpdate)
// 问题 3:存储了大图片数据
this.imageData = largeImageData
}
destroy() {
// 销毁方法未调用清理
this.container.innerHTML = ''
}
}修复方案:
class ImagePreview {
private resizeHandler = this.handleResize.bind(this)
private updateHandler = this.handleUpdate.bind(this)
private imageData: ImageData | null = null
constructor(private container: HTMLElement) {
window.addEventListener('resize', this.resizeHandler)
ipcRenderer.on('image-update', this.updateHandler)
}
destroy() {
// 移除事件监听器
window.removeEventListener('resize', this.resizeHandler)
ipcRenderer.removeListener('image-update', this.updateHandler)
// 释放大对象
this.imageData = null
// 清理 DOM
this.container.innerHTML = ''
}
}
// 确保销毁方法被调用
let preview: ImagePreview | null = null
function openPreview(container: HTMLElement) {
if (preview) {
preview.destroy()
}
preview = new ImagePreview(container)
}
function closePreview() {
if (preview) {
preview.destroy()
preview = null
}
}验证结果:
| 操作 | 修复前内存增长 | 修复后内存增长 |
|---|---|---|
| 打开/关闭 10 次 | +150 MB | +5 MB |
| 打开/关闭 50 次 | +750 MB | +8 MB |
场景:实时显示 10000+ 数据点的监控面板。
问题:每秒更新数据,内存持续增长,30 分钟后内存从 200MB 增长到 1GB。
根因:
// 问题代码
class MonitorPanel {
private dataPoints: DataPoint[] = []
update(newData: DataPoint[]) {
// 不断添加数据,从不清理
this.dataPoints.push(...newData)
// 重新渲染整个列表
this.render(this.dataPoints)
}
}优化方案:
class MonitorPanel {
private maxDataPoints = 1000 // 限制最大数据点数
private dataPoints: DataPoint[] = []
update(newData: DataPoint[]) {
// 添加新数据
this.dataPoints.push(...newData)
// 超出限制时移除旧数据
if (this.dataPoints.length > this.maxDataPoints) {
this.dataPoints = this.dataPoints.slice(-this.maxDataPoints)
}
// 使用虚拟列表渲染
this.render(this.dataPoints)
}
// 使用 requestAnimationFrame 节流渲染
private renderQueue = false
render(data: DataPoint[]) {
if (this.renderQueue) return
this.renderQueue = true
requestAnimationFrame(() => {
// 实际渲染逻辑
this.doRender(data)
this.renderQueue = false
})
}
}// 生产环境内存监控
class ProductionMemoryMonitor {
private threshold = 1024 * 1024 * 1024 // 1GB
start() {
setInterval(() => {
const usage = process.memoryUsage()
if (usage.heapUsed > this.threshold) {
this.handleMemoryPressure(usage)
}
}, 60000)
}
private handleMemoryPressure(usage: NodeJS.MemoryUsage) {
// 1. 记录日志
console.error('Memory pressure detected:', usage)
// 2. 触发 GC(如果可用)
if (global.gc) {
global.gc()
}
// 3. 清理非必要缓存
this.clearNonEssentialCaches()
// 4. 上报告警
this.reportAlert(usage)
}
private clearNonEssentialCaches() {
// 清理缓存逻辑
}
private reportAlert(usage: NodeJS.MemoryUsage) {
// 上报到监控系统
}
}诊断步骤:
判断标准:如果重复操作后内存持续增长,且手动 GC 后不下降,则存在泄漏。
| 场景 | 正常范围 | 警告阈值 |
|---|---|---|
| 空闲状态 | < 200 MB | > 500 MB |
| 正常使用 | 200-500 MB | > 800 MB |
| 大数据处理 | 500MB-1GB | > 1.5 GB |
| 主进程 | < 100 MB | > 300 MB |
// 使用流式处理
import { createReadStream } from 'fs'
import { createInterface } from 'readline'
async function processLargeFile(filePath: string) {
const stream = createReadStream(filePath)
const rl = createInterface({
input: stream,
crlfDelay: Infinity
})
let lineCount = 0
for await (const line of rl) {
// 逐行处理,不占用大量内存
processLine(line)
lineCount++
}
return lineCount
}
// 或者使用 Web Worker 在后台线程处理
const worker = new Worker('file-processor.js')
worker.postMessage({ file: largeFile })
worker.onmessage = (e) => {
console.log('Processed:', e.data)
}// 方案 1: 虚拟滚动(推荐)
// 只渲染可视区域内的节点
// 方案 2: 分页加载
// 按页加载数据,减少 DOM 节点
// 方案 3: 文档片段
const fragment = document.createDocumentFragment()
items.forEach(item => {
fragment.appendChild(createElement(item))
})
container.appendChild(fragment) // 单次重排
// 方案 4: 使用 requestAnimationFrame 分批渲染
function renderInBatches(items: any[], batchSize = 50) {
let index = 0
function renderBatch() {
const end = Math.min(index + batchSize, items.length)
while (index < end) {
container.appendChild(createElement(items[index]))
index++
}
if (index < items.length) {
requestAnimationFrame(renderBatch)
}
}
requestAnimationFrame(renderBatch)
}@electron/remote 会在渲染进程创建代理对象,可能导致循环引用。
解决方案:
// 避免使用 @electron/remote
// ❌
import remote from '@electron/remote'
const win = remote.getCurrentWindow()
// ✅ 使用 IPC
// preload.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('api', {
getWindowId: () => ipcRenderer.invoke('get-window-id'),
minimizeWindow: () => ipcRenderer.invoke('minimize-window')
})
// main.ts
ipcMain.handle('minimize-window', (event) => {
BrowserWindow.fromWebContents(event.sender)?.minimize()
})
// renderer.ts
await window.api.minimizeWindow()