启动优化
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 个模块加载 | 320ms | 85ms | 73% |
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-cachetypescript
// 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% |
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%优化措施:
- 延迟加载 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 })- 插件按需初始化
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)- 预编译模块快照
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
}