跳到主要内容

内存优化

问题

前端如何进行内存优化?常见的内存泄漏场景有哪些?如何检测和修复内存问题?

答案

内存优化是提升应用稳定性的关键。内存问题会导致页面卡顿、崩溃,特别是在长时间运行的 SPA 应用中。


内存管理基础

JavaScript 内存生命周期

阶段说明自动/手动
分配声明变量、创建对象自动
使用读写内存手动
释放垃圾回收自动(GC)

V8 垃圾回收

区域大小对象类型GC 算法
新生代1-8MB新对象、短生命周期Scavenge
老生代~1.4GB存活久的对象Mark-Sweep/Compact

常见内存泄漏场景

1. 意外的全局变量

// ❌ 意外创建全局变量
function leak() {
leakedVar = 'I am global'; // 忘记 const/let
this.anotherLeak = 'leak'; // 非严格模式下 this 指向 window
}

// ✅ 使用严格模式和明确声明
'use strict';
function safe() {
const localVar = 'I am local';
}

2. 未清除的定时器

// ❌ 组件销毁时未清除定时器
function LeakyComponent() {
useEffect(() => {
setInterval(() => {
console.log('Still running...');
}, 1000);
}, []);
}

// ✅ 清除定时器
function SafeComponent() {
useEffect(() => {
const timer = setInterval(() => {
console.log('Running...');
}, 1000);

return () => clearInterval(timer); // 清理
}, []);
}

3. 未移除的事件监听

// ❌ 未移除事件监听
function LeakyComponent() {
useEffect(() => {
window.addEventListener('resize', handleResize);
}, []);
}

// ✅ 移除事件监听
function SafeComponent() {
useEffect(() => {
window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
}

4. 闭包引用

// ❌ 闭包持有大对象
function createLeak() {
const largeData = new Array(1000000).fill('x');

return function() {
// 即使只用 largeData.length,整个数组都被保留
console.log(largeData.length);
};
}

// ✅ 只保留需要的数据
function createSafe() {
const largeData = new Array(1000000).fill('x');
const length = largeData.length;
// largeData 可被回收

return function() {
console.log(length);
};
}

5. 游离的 DOM 引用

// ❌ 保持对已删除 DOM 的引用
const elements: HTMLElement[] = [];

function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
elements.push(div); // 保持引用
}

function removeElements() {
elements.forEach(el => el.remove());
// elements 数组仍然引用这些 DOM 节点
}

// ✅ 同时清除引用
function removeElementsSafe() {
elements.forEach(el => el.remove());
elements.length = 0; // 清空数组
}

6. 未清理的 AbortController

// ❌ 未取消 fetch 请求
function LeakyComponent() {
useEffect(() => {
fetch('/api/data').then(setData);
}, []);
}

// ✅ 使用 AbortController
function SafeComponent() {
useEffect(() => {
const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') throw err;
});

return () => controller.abort();
}, []);
}

使用 WeakMap/WeakSet

WeakMap 和 WeakSet 的键是弱引用,不会阻止垃圾回收。

// ❌ Map 会阻止 key 对象被回收
const cache = new Map<object, string>();
let obj = { name: 'test' };
cache.set(obj, 'cached');
obj = null; // cache 仍然引用原对象,无法回收

// ✅ WeakMap 不阻止回收
const weakCache = new WeakMap<object, string>();
let obj2 = { name: 'test' };
weakCache.set(obj2, 'cached');
obj2 = null; // 对象可被回收,WeakMap 中的条目自动移除

实际应用

// DOM 元素元数据缓存
const metadata = new WeakMap<HTMLElement, object>();

function setMetadata(element: HTMLElement, data: object) {
metadata.set(element, data);
}

function getMetadata(element: HTMLElement) {
return metadata.get(element);
}
// 当 DOM 元素被移除时,关联的数据自动被回收

// 私有属性实现
const privateData = new WeakMap<object, { secret: string }>();

class MyClass {
constructor(secret: string) {
privateData.set(this, { secret });
}

getSecret() {
return privateData.get(this)?.secret;
}
}

对象池模式

对于频繁创建销毁的对象,使用对象池复用可以减少 GC 压力。

class ObjectPool<T> {
private pool: T[] = [];
private createFn: () => T;
private resetFn: (obj: T) => void;

constructor(
createFn: () => T,
resetFn: (obj: T) => void,
initialSize = 10
) {
this.createFn = createFn;
this.resetFn = resetFn;

// 预创建对象
for (let i = 0; i < initialSize; i++) {
this.pool.push(createFn());
}
}

acquire(): T {
return this.pool.pop() || this.createFn();
}

release(obj: T): void {
this.resetFn(obj);
this.pool.push(obj);
}
}

// 使用示例:粒子系统
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
life: number;
}

const particlePool = new ObjectPool<Particle>(
() => ({ x: 0, y: 0, vx: 0, vy: 0, life: 0 }),
(p) => { p.x = p.y = p.vx = p.vy = p.life = 0; }
);

function createParticle() {
const particle = particlePool.acquire();
particle.x = Math.random() * 100;
particle.y = Math.random() * 100;
particle.life = 100;
return particle;
}

function destroyParticle(particle: Particle) {
particlePool.release(particle);
}

大数据处理优化

分片处理

// ❌ 一次性处理大数据会阻塞
function processAllBad(data: number[]) {
return data.map(x => heavyComputation(x));
}

// ✅ 分片处理
async function processInChunks<T, R>(
data: T[],
processor: (item: T) => R,
chunkSize = 1000
): Promise<R[]> {
const results: R[] = [];

for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
const chunkResults = chunk.map(processor);
results.push(...chunkResults);

// 让出主线程
await new Promise(resolve => setTimeout(resolve, 0));
}

return results;
}

// 使用 requestIdleCallback
function processWhenIdle<T>(
items: T[],
processor: (item: T) => void,
callback: () => void
) {
let index = 0;

function process(deadline: IdleDeadline) {
while (index < items.length && deadline.timeRemaining() > 0) {
processor(items[index]);
index++;
}

if (index < items.length) {
requestIdleCallback(process);
} else {
callback();
}
}

requestIdleCallback(process);
}

使用 Web Worker

// worker.ts
self.onmessage = (e: MessageEvent<number[]>) => {
const result = e.data.map(x => x * 2);
self.postMessage(result);
};

// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));

worker.postMessage(largeArray);
worker.onmessage = (e) => {
console.log('Result:', e.data);
};

React 内存优化

// 1. 清理 useEffect
function Component() {
useEffect(() => {
const subscription = subscribe();

return () => {
subscription.unsubscribe();
};
}, []);
}

// 2. 避免在渲染中创建新对象
// ❌
function Bad() {
return <Child style={{ color: 'red' }} />; // 每次渲染创建新对象
}

// ✅
const styles = { color: 'red' };
function Good() {
return <Child style={styles} />;
}

// 3. 使用 useMemo 缓存大型计算结果
function Component({ data }) {
const processed = useMemo(() => {
return heavyProcess(data);
}, [data]);
}

// 4. 虚拟列表处理大量数据
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
return (
<FixedSizeList
height={400}
width={300}
itemCount={items.length}
itemSize={50}
>
{({ index, style }) => (
<div style={style}>{items[index]}</div>
)}
</FixedSizeList>
);
}

内存检测工具

Chrome DevTools Memory

// 1. Heap snapshot - 堆快照
// 查看当前内存中的对象

// 2. Allocation instrumentation - 分配追踪
// 追踪内存分配

// 3. Allocation sampling - 分配采样
// 采样分析内存分配

// 使用步骤:
// 1. 打开 DevTools > Memory
// 2. 选择 "Heap snapshot"
// 3. 点击 "Take snapshot"
// 4. 执行可能泄漏的操作
// 5. 再次快照
// 6. 比较两次快照的差异

Performance Monitor

// DevTools > More tools > Performance Monitor
// 实时监控:
// - JS heap size
// - DOM Nodes
// - JS event listeners
// - Documents

代码中检测

// 检测内存使用
if (performance.memory) {
console.log('Used JS Heap:', performance.memory.usedJSHeapSize);
console.log('Total JS Heap:', performance.memory.totalJSHeapSize);
console.log('Heap Limit:', performance.memory.jsHeapSizeLimit);
}

// 监控内存增长
let lastHeapSize = 0;

setInterval(() => {
if (performance.memory) {
const currentSize = performance.memory.usedJSHeapSize;
const diff = currentSize - lastHeapSize;

if (diff > 1000000) { // 增长超过 1MB
console.warn('Memory increased by', (diff / 1024 / 1024).toFixed(2), 'MB');
}

lastHeapSize = currentSize;
}
}, 5000);

常见面试问题

Q1: 常见的内存泄漏场景有哪些?

答案

场景原因解决方案
全局变量忘记声明使用严格模式
定时器未清除clearInterval/clearTimeout
事件监听未移除removeEventListener
闭包持有大对象只保留必要数据
DOM 引用保留已删除 DOM清空引用
未完成请求fetch/XHR 未取消AbortController

Q2: 如何检测内存泄漏?

答案

// 1. Chrome DevTools
// Memory > Heap snapshot
// 操作前后对比,查看增长的对象

// 2. Performance Monitor
// 观察 JS heap size 是否持续增长

// 3. 代码监控
function detectLeak() {
const snapshots: number[] = [];

setInterval(() => {
if (performance.memory) {
snapshots.push(performance.memory.usedJSHeapSize);

// 检测持续增长趋势
if (snapshots.length > 10) {
const trend = snapshots.slice(-10).every((v, i, arr) =>
i === 0 || v > arr[i - 1]
);

if (trend) {
console.warn('Possible memory leak detected');
}
}
}
}, 10000);
}

Q3: WeakMap 和 Map 的区别?

答案

特性MapWeakMap
键类型任意只能是对象
引用类型强引用弱引用
可迭代
size 属性
垃圾回收阻止不阻止
适用场景通用缓存DOM 元数据、私有属性

Q4: 如何优化大数据处理?

答案

// 1. 分片处理
async function processChunks(data: any[], chunkSize = 1000) {
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
processChunk(chunk);
await new Promise(r => setTimeout(r, 0));
}
}

// 2. 使用 Web Worker
const worker = new Worker('worker.js');
worker.postMessage(largeData);

// 3. 虚拟列表
// 只渲染可见区域

// 4. 流式处理
// 使用 ReadableStream 逐步处理

// 5. 对象池
// 复用对象,减少 GC

Q5: React 中如何避免内存泄漏?

答案

function SafeComponent() {
const [data, setData] = useState(null);

useEffect(() => {
let isMounted = true;
const controller = new AbortController();

// 1. 异步操作的清理
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(data => {
if (isMounted) setData(data);
});

// 2. 事件监听清理
window.addEventListener('resize', handleResize);

// 3. 定时器清理
const timer = setInterval(() => {}, 1000);

// 4. 订阅清理
const subscription = eventBus.subscribe(handler);

return () => {
isMounted = false;
controller.abort();
window.removeEventListener('resize', handleResize);
clearInterval(timer);
subscription.unsubscribe();
};
}, []);
}

相关链接