内存优化
问题
前端如何进行内存优化?常见的内存泄漏场景有哪些?如何检测和修复内存问题?
答案
内存优化是提升应用稳定性的关键。内存问题会导致页面卡顿、崩溃,特别是在长时间运行的 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 的区别?
答案:
| 特性 | Map | WeakMap |
|---|---|---|
| 键类型 | 任意 | 只能是对象 |
| 引用类型 | 强引用 | 弱引用 |
| 可迭代 | ✅ | ❌ |
| 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();
};
}, []);
}