{T}

启动优化

Electron 应用的启动速度直接影响用户的第一印象。本文档深入分析启动流程,提供系统化的优化策略。

启动流程分析

完整启动时序

plaintext
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

性能测量方法

typescript
// 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:首次渲染
win.webContents.on('did-render-ready', () => {
  console.log(`[Perf] First render: ${Date.now() - startTime}ms`)
})
typescript
// 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
  })
})

主进程优化

1. 延迟加载模块

问题:在主进程顶部导入所有依赖会导致启动时同步加载所有模块。

typescript
// ❌ 启动时加载所有模块
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 个模块加载320ms85ms73%

2. 异步初始化模式

typescript
// 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')
  // ... 托盘初始化
}

3. 条件性加载

typescript
// 根据功能开关决定是否加载
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())
}

窗口创建优化

1. ready-to-show 事件

避免显示空白窗口,等待页面准备就绪后再显示:

typescript
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')
})

2. 窗口预热策略

预创建可能使用的窗口:

typescript
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)
  }
}

3. 窗口池模式

适用于频繁创建销毁的同类型窗口:

typescript
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)
  })
}

4. 窗口配置优化

typescript
const win = new BrowserWindow({
  // 减少不必要的功能
  autoHideMenuBar: true,        // 隐藏菜单栏
  frame: true,                  // 保留原生边框(更快)
  transparent: false,           // 避免透明窗口开销
  
  // 优化渲染
  backgroundColor: '#fff',      // 设置背景色避免闪烁
  webPreferences: {
    nodeIntegration: false,     // 安全 + 性能
    contextIsolation: true,
    enableRemoteModule: false,  // 禁用 remote 模块
    spellcheck: false,          // 禁用拼写检查(如不需要)
    devTools: isDev             // 仅开发环境开启
  }
})

渲染进程优化

1. 代码分割与懒加载

typescript
// 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
})

2. 首屏资源优化

html
<!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>

3. 骨架屏实现

Vue SFC
<!-- 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>

4. 数据预加载

typescript
// 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 编译优化

1. v8-compile-cache

缓存 V8 编译结果,加速模块加载:

bash
npm install v8-compile-cache
typescript
// main.ts - 必须在其他 require 之前
import 'v8-compile-cache'
 
// 或者
require('v8-compile-cache')
 
// 后续的 require 会自动使用缓存
import { app, BrowserWindow } from 'electron'

效果

模块数量无缓存有缓存提升
50 个450ms180ms60%
100 个820ms310ms62%

2. V8 快照(高级)

预编译模块到 V8 快照:

typescript
// 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 操作)。


构建优化

1. Vite 配置优化

typescript
// 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']
  }
})

2. Webpack 配置优化

typescript
// 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'  // 根据实际情况
  }
}

3. 资源压缩

typescript
// 图片压缩脚本
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] })
  ]
})

性能监控

启动性能采集

typescript
// 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()

持续性能监控

typescript
// 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 秒,用户体验差。

问题分析

plaintext
阶段              耗时      占比
─────────────────────────────
模块加载          1800ms   36%
窗口创建          800ms    16%
Monaco Editor     1200ms   24%
插件初始化        700ms    14%
首帧渲染          500ms    10%
─────────────────────────────
总计              5000ms   100%

优化措施

  1. 延迟加载 Monaco Editor
typescript
// 仅在需要时加载编辑器
const editorContainer = document.getElementById('editor')
editorContainer.addEventListener('click', async () => {
  const monaco = await import('monaco-editor')
  monaco.editor.create(editorContainer, { /* config */ })
}, { once: true })
  1. 插件按需初始化
typescript
// 核心插件立即初始化,其他延迟
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)
  1. 预编译模块快照
typescript
// 对高频使用的模块生成 V8 快照
require('v8-compile-cache')

优化结果

plaintext
阶段              优化前    优化后    提升
───────────────────────────────────────
模块加载          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

常见问题

Q1: 窗口显示白屏怎么办?

使用 ready-to-show 事件和背景色:

typescript
const win = new BrowserWindow({
  show: false,
  backgroundColor: '#1e1e1e'  // 匹配应用主题
})
 
win.once('ready-to-show', () => {
  win.show()
})

Q2: 如何定位启动慢的模块?

使用性能分析:

typescript
// 在每个 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 面板录制启动过程。

Q3: 懒加载导致功能延迟怎么办?

采用预加载策略:

typescript
// 用户可能使用的功能提前加载
win.webContents.on('did-finish-load', () => {
  // 空闲时预加载
  requestIdleCallback(() => {
    import('./features/settings')
    import('./features/search')
  })
})

Q4: 如何在开发环境和生产环境使用不同的优化策略?

typescript
const isDev = process.env.NODE_ENV === 'development'
 
// 开发环境禁用某些优化以加快热更新
if (!isDev) {
  require('v8-compile-cache')
}
 
// 生产环境启用压缩
const config = {
  minify: !isDev,
  sourcemap: isDev
}

相关文档