C03 内存管理与性能分析
C03 内存管理与性能分析
摘要:内存由垃圾回收器自动回收,回收依据是可达性而非引用计数,因此闭包、全局缓存、未解绑的监听器都能造成滞留。定位手段是堆快照对比与分配时间线,优化手段是缩短引用生命周期与减少对象创建。主线程上的长任务同时影响帧率与交互延迟。本文给出三类泄漏复现、快照对比流程与四项降低分配的做法。
关键词:垃圾回收,内存泄漏,堆快照,长任务,性能分析
所属:frontend/js / C-高级 | 上一篇:C02-属性描述符与Proxy | 下一篇:C04-并发控制与取消
1 引言
单页应用打开数小时后变卡、标签页内存持续上涨、切换路由后旧页面的定时器仍在跑。三者属于同一类问题:对象本应被回收,却仍有引用可达。
本文回答三个问题:什么对象算可达、怎样用工具找到滞留对象、怎样降低主线程压力。范围见表 1-1。
| 项 | 本文覆盖 | 本文不覆盖 |
|---|---|---|
| 回收 | 可达性判定、常见滞留根 | 分代收集算法细节 |
| 定位 | 堆快照、分配时间线、performance.memory |
服务端内存分析 |
| 优化 | 对象复用、减少闭包捕获、长任务拆分 | 渲染性能(CSS 科目) |
2 预备知识
| 项 | 要求 | 核对方式 |
|---|---|---|
| 环境 | Chrome 130+,Memory 与 Performance 面板可用 | 能录制性能时间线 |
| 前置 | B01、B04 | 理解闭包与长任务 |
| 文件 | js-practice/memory.html |
编码 UTF-8 |
3 原理与实现
3.1 可达性判定
回收器从根对象出发沿引用遍历,遍历不到的对象才可回收,如图 3-1 所示。
flowchart TD
A["根:window / document / 活动栈帧"] --> B["模块作用域变量"]
A --> C["事件监听器表"]
B --> D["缓存 Map 中的旧页面数据"]
C --> E["已移除节点的回调闭包"]
D --> F["无法回收"]
E --> F
G["局部变量随函数返回出栈"] --> H["若无其它引用 → 可回收"]
图 3-1 从根出发的可达链决定对象能否被回收
图上可见:全局缓存与未解绑监听器是两条最长的可达链,也是泄漏最常见的两个来源。
3.2 三类泄漏的复现与修复
<!-- js-practice/memory.html;运行环境:Chrome 130+ -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>内存分析</title>
</head>
<body>
<button id="leak">制造泄漏</button>
<button id="fix">执行修复版</button>
<div id="mount"></div>
<script type="module" src="./memory.js"></script>
</body>
</html>
// js-practice/memory.js;运行环境:Chrome 130+
const mount = document.querySelector('#mount');
const cache = new Map(); // 泄漏源 1:无上限的全局缓存
// 泄漏源 1:缓存键只增不减
function leakByCache(index) {
const payload = new Array(10_000).fill(index);
cache.set(`page-${index}`, payload);
}
// 修复 1:限制容量,超出即淘汰最早的键
function safeCache(key, value, limit = 5) {
if (cache.size >= limit) cache.delete(cache.keys().next().value);
cache.set(key, value);
}
// 泄漏源 2:移除节点但监听器仍持有引用
function leakByListener() {
const node = document.createElement('div');
const big = new Array(50_000).fill('x');
node.addEventListener('click', () => console.log(big.length));
mount.append(node);
node.remove(); // 节点移除,监听器与 big 仍可达
}
// 修复 2:用 AbortController 统一解绑
const controller = new AbortController();
function safeListener() {
const node = document.createElement('div');
node.addEventListener('click', () => {}, { signal: controller.signal });
mount.append(node);
node.remove();
controller.abort();
}
// 泄漏源 3:定时器未清理
const timers = new Set();
function leakByTimer() {
const data = new Array(20_000).fill(1);
const id = setInterval(() => data.length, 1000);
timers.add(id); // 永不清理
}
// 修复 3:组件卸载时统一清理
function clearTimers() {
for (const id of timers) clearInterval(id);
timers.clear();
}
document.querySelector('#leak').addEventListener('click', () => {
for (let i = 0; i < 20; i += 1) {
leakByCache(i);
leakByListener();
}
leakByTimer();
});
document.querySelector('#fix').addEventListener('click', () => {
for (let i = 0; i < 20; i += 1) {
safeCache(`page-${i}`, new Array(10_000).fill(i));
safeListener();
}
});
四类滞留根与对策见表 3-1。
| 滞留根 | 表现 | 对策 |
|---|---|---|
| 全局缓存 | Map/数组只增不减 |
设容量上限与淘汰策略 |
| 事件监听器 | 节点已移除,回调仍持有大对象 | AbortController 统一解绑 |
| 定时器/动画帧 | 组件卸载后仍在执行 | 卸载时 clearInterval |
| 闭包捕获 | 回调引用整个大对象 | 只捕获必要字段 |
分析工具与用途见表 3-2。
| 工具 | 用途 | 判读依据 |
|---|---|---|
| 堆快照对比 | 找出两次快照间新增且未释放的对象 | 「大小增量」列的保留对象 |
| 分配时间线 | 观察一段时间内的分配与回收 | 蓝色柱持续上升即净增长 |
| Performance 火焰图 | 定位长任务与耗时函数 | 超过 50 毫秒的块 |
performance.memory |
粗看已用堆大小(仅 Chrome) | 只用于快速判断趋势 |
4 验证与讨论
- 连续点击「制造泄漏」十次,拍两次堆快照,在「比较」视图确认
Array与Detached HTMLElement数量增长。 - 刷新页面,改为点击「执行修复版」十次,再次对比,确认增长量显著下降。
- 在 Performance 面板录制点击过程,确认「制造泄漏」一次点击产生超过 50 毫秒的任务。
- 在控制台执行
performance.memory.usedJSHeapSize,观察点击前后的差值。
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| 缓存泄漏 | Array 实例数量随点击增长 |
键只增不减 |
| 监听器泄漏 | 快照中出现分离的 DOM 节点 | 回调持有引用 |
| 修复有效 | 增量回到基线附近 | 容量上限与解绑生效 |
| 长任务 | 单次点击任务超过 50 毫秒 | 大量对象同时创建 |
讨论与局限。 堆快照本身会暂停主线程并占用大量内存,只在复现问题时使用。另,浏览器在内存压力下会更早触发回收,因此「不涨内存」不代表实现正确,仍需按引用关系审查。
5 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 快照中对象数一直涨 | 存在可达链 | 从「保留器」面板回溯到根 |
| 移除节点后仍占内存 | 监听器或闭包持有 | 解绑监听器,缩小闭包捕获范围 |
| 切换路由后内存不降 | 定时器与订阅未清理 | 在卸载钩子中统一清理 |
| 页面越用越卡 | 长任务累积,帧率下降 | 拆分任务或移入 Worker |
performance.memory 数值波动大 |
采样时机不同导致 | 多次采样取趋势,不看单次值 |
6 结论
| 序号 | 结论 | 依据 |
|---|---|---|
| 1 | 回收依据是可达性,不是引用计数 | 图 3-1 |
| 2 | 全局缓存必须有容量上限 | 表 3-1 |
| 3 | 监听器与定时器需在卸载时统一清理 | 表 3-1 |
| 4 | 超过 50 毫秒的任务按长任务处理 | 表 3-2 |
- 上一篇:C02-属性描述符与Proxy
- 下一篇:C04-并发控制与取消
- 返回:C00-总结 | 科目 README
参考文献
[1] MDN Web Docs · 内存管理,https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_management(访问于 2026-10-31)
[2] Chrome DevTools · 解决内存问题,https://developer.chrome.com/docs/devtools/memory-problems(访问于 2026-10-31)

评论 0
还没有评论,快来抢沙发吧~