内存管理
Electron 应用基于 Chromium,内存占用通常较大。本文档深入分析内存结构、泄漏诊断和优化策略。
内存架构
进程内存分布
code
┌─────────────────────────────────────────────────────────────┐
│ 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 创建独立的渲染进程:
code
窗口数量 基础内存 增量
──────────────────────────
1 150MB -
2 250MB +100MB
3 350MB +100MB
4 450MB +100MB建议:控制同时打开的窗口数量,或采用窗口复用策略。
内存监控
主进程监控
typescript
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())渲染进程监控
typescript
// 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()跨进程内存统计
typescript
// 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`
}
}常见内存问题
1. 事件监听器泄漏
问题:未清理的事件监听器导致对象无法释放。
typescript
// ❌ 内存泄漏:重复添加监听器且不移除
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()
}
}2. 定时器泄漏
typescript
// ❌ 内存泄漏:未清理的定时器
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 时自动清理定时器3. 闭包泄漏
typescript
// ❌ 内存泄漏:闭包持有大对象
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
}
})
}
}4. DOM 节点泄漏
typescript
// ❌ 内存泄漏:保留已移除节点的引用
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
}
}5. 缓存无限增长
typescript
// ❌ 内存泄漏:缓存无限增长
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 时自动清理
}内存泄漏诊断
Chrome DevTools 诊断流程
1. 堆快照对比
code
步骤:
1. 打开 DevTools → Memory 面板
2. 选择 "Heap snapshot"
3. 执行以下操作:
- 快照 A:操作前
- 执行可能泄漏的操作(如打开/关闭窗口)
- 快照 B:操作后
4. 选择 "Comparison" 视图对比
5. 关注 "Delta" 为正的对象2. 时间线分配分析
code
步骤:
1. 打开 DevTools → Memory 面板
2. 选择 "Allocation instrumentation on timeline"
3. 开始录制
4. 执行操作
5. 停止录制
6. 查看蓝色柱状图(内存分配)
7. 点击柱状图查看分配位置3. 分配采样
code
步骤:
1. 打开 DevTools → Memory 面板
2. 选择 "Allocation sampling"
3. 开始采样
4. 执行操作
5. 停止采样
6. 查看 Call Tree 找出分配热点诊断脚本
typescript
// 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 调用 |
优化策略
1. 渲染进程数量控制
typescript
// 窗口复用管理器
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()
}
}2. 虚拟列表实现
Vue SFC
<!-- 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>3. 图片懒加载
Vue SFC
<!-- 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>4. 数据分页与懒加载
typescript
// 大数据分页加载
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
}
}案例分析
案例:图片查看器内存泄漏
现象:反复打开关闭图片查看器窗口,内存持续增长。
诊断过程:
- 使用 Chrome DevTools Memory 面板
- 在打开窗口前拍摄快照 A
- 打开并关闭窗口 5 次
- 拍摄快照 B
- 对比发现:
Detached HTMLDivElement数量增长 25 个ImagePreview组件实例增长 5 个- EventListener 数量增长 50 个
根因分析:
typescript
// 问题代码
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 = ''
}
}修复方案:
typescript
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。
根因:
typescript
// 问题代码
class MonitorPanel {
private dataPoints: DataPoint[] = []
update(newData: DataPoint[]) {
// 不断添加数据,从不清理
this.dataPoints.push(...newData)
// 重新渲染整个列表
this.render(this.dataPoints)
}
}优化方案:
typescript
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
})
}
}最佳实践
开发阶段
- 使用 ESLint 规则检测未清理的监听器
- 定期使用 DevTools Memory 面板检查
- 代码审查时关注资源清理逻辑
- 为所有组件实现销毁方法
测试阶段
- 自动化内存泄漏测试
- 长时间运行测试(24h+)
- 压力测试(大数据量、高频操作)
生产阶段
- 监控内存指标
- 设置内存告警阈值
- 实现内存异常自动恢复
typescript
// 生产环境内存监控
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) {
// 上报到监控系统
}
}常见问题
Q1: 如何确定内存泄漏?
诊断步骤:
- 打开 Chrome DevTools Memory 面板
- 拍摄堆快照 A
- 执行可能泄漏的操作(如打开/关闭窗口)
- 手动触发 GC(点击垃圾桶图标)
- 拍摄堆快照 B
- 选择 "Comparison" 视图
- 关注 Delta > 0 的对象
判断标准:如果重复操作后内存持续增长,且手动 GC 后不下降,则存在泄漏。
Q2: 内存使用多少算正常?
| 场景 | 正常范围 | 警告阈值 |
|---|---|---|
| 空闲状态 | < 200 MB | > 500 MB |
| 正常使用 | 200-500 MB | > 800 MB |
| 大数据处理 | 500MB-1GB | > 1.5 GB |
| 主进程 | < 100 MB | > 300 MB |
Q3: 如何处理大文件加载?
typescript
// 使用流式处理
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)
}Q4: 如何优化大量 DOM 节点?
typescript
// 方案 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)
}Q5: @electron/remote 导致内存泄漏怎么办?
@electron/remote 会在渲染进程创建代理对象,可能导致循环引用。
解决方案:
typescript
// 避免使用 @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()