C03 内存管理与性能分析

2026-10-31 17:070 阅读约 12 分钟JavaScript

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 验证与讨论

  1. 连续点击「制造泄漏」十次,拍两次堆快照,在「比较」视图确认 Array 与 Detached HTMLElement 数量增长。
  2. 刷新页面,改为点击「执行修复版」十次,再次对比,确认增长量显著下降。
  3. 在 Performance 面板录制点击过程,确认「制造泄漏」一次点击产生超过 50 毫秒的任务。
  4. 在控制台执行 performance.memory.usedJSHeapSize,观察点击前后的差值。
检查项 预期结果 说明
缓存泄漏 Array 实例数量随点击增长 键只增不减
监听器泄漏 快照中出现分离的 DOM 节点 回调持有引用
修复有效 增量回到基线附近 容量上限与解绑生效
长任务 单次点击任务超过 50 毫秒 大量对象同时创建

讨论与局限。 堆快照本身会暂停主线程并占用大量内存,只在复现问题时使用。另,浏览器在内存压力下会更早触发回收,因此「不涨内存」不代表实现正确,仍需按引用关系审查。

5 常见问题

现象 原因 处理
快照中对象数一直涨 存在可达链 从「保留器」面板回溯到根
移除节点后仍占内存 监听器或闭包持有 解绑监听器,缩小闭包捕获范围
切换路由后内存不降 定时器与订阅未清理 在卸载钩子中统一清理
页面越用越卡 长任务累积,帧率下降 拆分任务或移入 Worker
performance.memory 数值波动大 采样时机不同导致 多次采样取趋势,不看单次值

6 结论

序号 结论 依据
1 回收依据是可达性,不是引用计数 图 3-1
2 全局缓存必须有容量上限 表 3-1
3 监听器与定时器需在卸载时统一清理 表 3-1
4 超过 50 毫秒的任务按长任务处理 表 3-2

参考文献

[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

0/2000

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