{T}

内存管理

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常驻内存集应用实际占用
heapTotalV8 堆总量-
heapUsedV8 堆使用量< heapTotal 的 70%
external外部内存 (Buffer)-
arrayBuffersArrayBuffer 内存-

多渲染进程内存影响

每个 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
  }
}

案例分析

案例:图片查看器内存泄漏

现象:反复打开关闭图片查看器窗口,内存持续增长。

诊断过程

  1. 使用 Chrome DevTools Memory 面板
  2. 在打开窗口前拍摄快照 A
  3. 打开并关闭窗口 5 次
  4. 拍摄快照 B
  5. 对比发现:
    • 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: 如何确定内存泄漏?

诊断步骤

  1. 打开 Chrome DevTools Memory 面板
  2. 拍摄堆快照 A
  3. 执行可能泄漏的操作(如打开/关闭窗口)
  4. 手动触发 GC(点击垃圾桶图标)
  5. 拍摄堆快照 B
  6. 选择 "Comparison" 视图
  7. 关注 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()

相关文档

参考资料