[{"data":1,"prerenderedAt":1000},["ShallowReactive",2],{"page-/post/performance/debugging-memory-leaks-with-chrome-devtools":3,"surrounding-page":991},{"id":4,"title":5,"author":6,"body":7,"date":981,"description":57,"extension":982,"group":6,"lastmod":983,"meta":984,"navigation":985,"path":986,"rawbody":987,"seo":988,"showTitle":6,"stem":989,"tags":6,"versions":6,"__hash__":990},"content/post/performance/debugging-memory-leaks-with-chrome-devtools.md","使用 Chrome DevTools 排查内存泄漏",null,{"type":8,"value":9,"toc":942},"minimark",[10,14,18,21,25,29,45,48,51,159,161,165,168,195,198,204,212,218,224,230,238,240,244,247,252,276,281,358,361,367,373,375,379,383,389,398,402,407,413,418,436,441,447,456,458,462,465,469,474,480,486,495,497,501,505,511,516,532,534,538,542,548,553,562,564,568,572,578,583,592,594,598,602,608,613,621,623,627,631,634,645,649,655,659,662,693,697,700,729,731,735,741,804,806,810,813,845,848,882,885,908,910,913,938],[11,12,13],"h2",{"id":13},"概述",[15,16,17],"p",{},"内存泄漏是前端性能优化中最难排查的问题之一。本文将详细介绍如何使用 Chrome DevTools 的 Performance 和 Memory 面板，系统化地排查并定位内存泄漏问题，并映射到真实代码逻辑。",[19,20],"hr",{},[11,22,24],{"id":23},"一内存泄漏的识别","一、内存泄漏的识别",[26,27,28],"h3",{"id":28},"典型症状",[30,31,32,36,39,42],"ul",{},[33,34,35],"li",{},"页面运行一段时间后变卡",[33,37,38],{},"滚动、点击响应变慢",[33,40,41],{},"浏览器标签页显示内存占用持续增长",[33,43,44],{},"最终页面崩溃（Out of Memory）",[26,46,47],{"id":47},"快速检测",[15,49,50],{},"在控制台运行以下代码,观察内存是否持续增长：",[52,53,58],"pre",{"className":54,"code":55,"language":56,"meta":57,"style":57},"language-javascript shiki shiki-themes github-light","setInterval(() => {\n  const usedMB = performance.memory.usedJSHeapSize / 1024 / 1024\n  console.log(`当前内存占用: ${usedMB.toFixed(2)} MB`)\n}, 1000)\n","javascript","",[59,60,61,81,109,148],"code",{"__ignoreMap":57},[62,63,66,70,74,78],"span",{"class":64,"line":65},"line",1,[62,67,69],{"class":68},"s7eDp","setInterval",[62,71,73],{"class":72},"sgsFI","(() ",[62,75,77],{"class":76},"sD7c4","=>",[62,79,80],{"class":72}," {\n",[62,82,84,87,91,94,97,100,103,106],{"class":64,"line":83},2,[62,85,86],{"class":76},"  const",[62,88,90],{"class":89},"sYu0t"," usedMB",[62,92,93],{"class":76}," =",[62,95,96],{"class":72}," performance.memory.usedJSHeapSize ",[62,98,99],{"class":76},"/",[62,101,102],{"class":89}," 1024",[62,104,105],{"class":76}," /",[62,107,108],{"class":89}," 1024\n",[62,110,112,115,118,121,125,128,131,134,136,139,142,145],{"class":64,"line":111},3,[62,113,114],{"class":72},"  console.",[62,116,117],{"class":68},"log",[62,119,120],{"class":72},"(",[62,122,124],{"class":123},"sYBdl","`当前内存占用: ${",[62,126,127],{"class":72},"usedMB",[62,129,130],{"class":123},".",[62,132,133],{"class":68},"toFixed",[62,135,120],{"class":123},[62,137,138],{"class":89},"2",[62,140,141],{"class":123},")",[62,143,144],{"class":123},"} MB`",[62,146,147],{"class":72},")\n",[62,149,151,154,157],{"class":64,"line":150},4,[62,152,153],{"class":72},"}, ",[62,155,156],{"class":89},"1000",[62,158,147],{"class":72},[19,160],{},[11,162,164],{"id":163},"二performance-面板确认是否泄漏","二、Performance 面板：确认是否泄漏",[26,166,167],{"id":167},"录制内存快照",[169,170,171,174,182,189,192],"ol",{},[33,172,173],{},"打开 DevTools (F12) → Performance 标签",[33,175,176,177,181],{},"勾选 ",[178,179,180],"strong",{},"Memory"," 选项",[33,183,184,185,188],{},"点击 ",[178,186,187],{},"Record"," 录制 30-60 秒",[33,190,191],{},"执行可疑操作（滚动列表、打开关闭弹窗等）",[33,193,194],{},"停止录制",[26,196,197],{"id":197},"分析内存走势图",[15,199,200,203],{},[178,201,202],{},"正常情况","（有涨有跌,GC 能回收）：",[52,205,210],{"className":206,"code":208,"language":209},[207],"language-text","Memory (MB)\n  ↑     ╱╲      ╱╲      ╱╲\n  │    ╱  ╲    ╱  ╲    ╱  ╲\n  └─────────────────────────→ 时间\n","text",[59,211,208],{"__ignoreMap":57},[15,213,214,217],{},[178,215,216],{},"内存泄漏","（持续上涨,呈阶梯状）：",[52,219,222],{"className":220,"code":221,"language":209},[207],"Memory (MB)\n  ↑   ╱──────╱─────╱─────╱──\n  │  ╱      ╱     ╱     ╱\n  └─────────────────────────→ 时间\n",[59,223,221],{"__ignoreMap":57},[15,225,226,229],{},[178,227,228],{},"判断标准","：",[30,231,232,235],{},[33,233,234],{},"✅ 正常：内存有涨有跌,GC 后能降下来",[33,236,237],{},"❌ 泄漏：内存持续上涨,GC 后仍然增长",[19,239],{},[11,241,243],{"id":242},"三memory-面板定位泄漏点","三、Memory 面板：定位泄漏点",[26,245,246],{"id":246},"对比堆快照",[15,248,249,229],{},[178,250,251],{},"操作流程",[169,253,254,257,260,263,266,269],{},[33,255,256],{},"DevTools → Memory 标签 → 选择 \"Heap snapshot\"",[33,258,259],{},"点击 \"Take snapshot\" → 获得快照 1",[33,261,262],{},"执行可疑操作（如打开 10 次弹窗后关闭）",[33,264,265],{},"强制垃圾回收（点击 🗑️ 图标）",[33,267,268],{},"点击 \"Take snapshot\" → 获得快照 2",[33,270,271,272,275],{},"切换视图为 ",[178,273,274],{},"Comparison"," → 选择 \"between Snapshot 1 and Snapshot 2\"",[15,277,278,229],{},[178,279,280],{},"关键列说明",[282,283,284,300],"table",{},[285,286,287],"thead",{},[288,289,290,294,297],"tr",{},[291,292,293],"th",{},"列名",[291,295,296],{},"含义",[291,298,299],{},"关注点",[301,302,303,317,332,345],"tbody",{},[288,304,305,311,314],{},[306,307,308],"td",{},[178,309,310],{},"# Delta",[306,312,313],{},"净增加的对象数量",[306,315,316],{},"应该接近 0",[288,318,319,324,327],{},[306,320,321],{},[178,322,323],{},"Size Delta",[306,325,326],{},"净增加的内存",[306,328,329],{},[178,330,331],{},"最关键的指标",[288,333,334,339,342],{},[306,335,336],{},[178,337,338],{},"Alloc. Size",[306,340,341],{},"新增对象占用内存",[306,343,344],{},"持续增长说明泄漏",[288,346,347,352,355],{},[306,348,349],{},[178,350,351],{},"Freed Size",[306,353,354],{},"释放的内存",[306,356,357],{},"应该接近 Alloc. Size",[26,359,360],{"id":360},"查找泄漏对象",[15,362,363,364,366],{},"按 ",[178,365,323],{}," 排序,找到占用内存最多的对象类型：",[52,368,371],{"className":369,"code":370,"language":209},[207],"Constructor              # Delta    Size Delta\n─────────────────────────────────────────────\n(array)                  +500       +2.5 MB    ← 可疑！数组持续增长\nDetached HTMLDivElement  +200       +800 KB    ← DOM 泄漏\nEventListener            +150       +150 KB    ← 事件监听器未移除\n",[59,372,370],{"__ignoreMap":57},[19,374],{},[11,376,378],{"id":377},"四定位到真实代码","四、定位到真实代码",[26,380,382],{"id":381},"查看-retainers保留路径","查看 Retainers（保留路径）",[15,384,385,388],{},[178,386,387],{},"这是最关键的环节！"," Retainers 显示了为什么这个对象没有被垃圾回收。",[15,390,391,394,395],{},[178,392,393],{},"操作","：点击可疑对象 → 选择具体实例 → 右侧面板显示 ",[178,396,397],{},"Retainers",[26,399,401],{"id":400},"解读-retainers-路径","解读 Retainers 路径",[15,403,404],{},[178,405,406],{},"示例 1：全局变量引用",[52,408,411],{"className":409,"code":410,"language":209},[207],"Retainers:\n  → Window / http://localhost:3000\n    → VueComponent                ← Vue 组件实例\n      → setupState                ← setup() 返回的状态\n        → allData                  ← 你的变量名\n          → @123456 (array)       ← 泄漏的数组\n",[59,412,410],{"__ignoreMap":57},[15,414,415,229],{},[178,416,417],{},"如何对应到代码",[30,419,420,430],{},[33,421,422,423,426,427],{},"看到 ",[59,424,425],{},"allData"," → 在代码中搜索 ",[59,428,429],{},"const allData = ref(...)",[33,431,422,432,435],{},[59,433,434],{},"VueComponent"," → 定位到具体的组件文件",[15,437,438],{},[178,439,440],{},"示例 2：事件监听器引用",[52,442,445],{"className":443,"code":444,"language":209},[207],"Retainers:\n  → Window\n    → eventListeners          ← 全局事件监听器映射\n      → scroll                ← scroll 事件\n        → [[Handler]]\n          → VueComponent      ← 组件实例被闭包引用\n",[59,446,444],{"__ignoreMap":57},[15,448,449,229,452,455],{},[178,450,451],{},"结论",[59,453,454],{},"scroll"," 事件监听器没有被移除,闭包引用了组件实例。",[19,457],{},[11,459,461],{"id":460},"五常见内存泄漏模式","五、常见内存泄漏模式",[15,463,464],{},"通过 Memory 面板,常见的泄漏对象类型：",[26,466,468],{"id":467},"_51-dom-泄漏","5.1 DOM 泄漏",[15,470,471,229],{},[178,472,473],{},"现象",[52,475,478],{"className":476,"code":477,"language":209},[207],"Constructor              # Delta    Size Delta\n────────────────────────────────────────────\nDetached HTMLDivElement  +200       +800 KB\n",[59,479,477],{"__ignoreMap":57},[15,481,482,485],{},[178,483,484],{},"原因","：移除 DOM 时未清理事件监听器,导致元素无法被 GC",[15,487,488,491,492],{},[178,489,490],{},"解决","：在移除 DOM 前调用 ",[59,493,494],{},"removeEventListener",[19,496],{},[26,498,500],{"id":499},"_52-定时器泄漏","5.2 定时器泄漏",[15,502,503,229],{},[178,504,473],{},[52,506,509],{"className":507,"code":508,"language":209},[207],"Constructor    # Delta    Size Delta\n────────────────────────────────────\nTimeout        +50        +100 KB\n",[59,510,508],{"__ignoreMap":57},[15,512,513,515],{},[178,514,484],{},"：组件销毁时定时器仍在运行",[15,517,518,520,521,524,525,528,529],{},[178,519,490],{},"：在 ",[59,522,523],{},"onUnmounted"," 中调用 ",[59,526,527],{},"clearInterval"," / ",[59,530,531],{},"clearTimeout",[19,533],{},[26,535,537],{"id":536},"_53-事件监听器泄漏","5.3 事件监听器泄漏",[15,539,540,229],{},[178,541,473],{},[52,543,546],{"className":544,"code":545,"language":209},[207],"Constructor      # Delta    Size Delta\n──────────────────────────────────────\nEventListener    +100       +200 KB\n",[59,547,545],{"__ignoreMap":57},[15,549,550,552],{},[178,551,484],{},"：全局事件监听器未移除",[15,554,555,520,557,524,559],{},[178,556,490],{},[59,558,523],{},[59,560,561],{},"window.removeEventListener",[19,563],{},[26,565,567],{"id":566},"_54-组件实例泄漏","5.4 组件实例泄漏",[15,569,570,229],{},[178,571,473],{},[52,573,576],{"className":574,"code":575,"language":209},[207],"Constructor        # Delta    Size Delta\n────────────────────────────────────────\nVueComponent       +50        +5 MB\n",[59,577,575],{"__ignoreMap":57},[15,579,580,582],{},[178,581,484],{},"：事件总线监听器未注销,导致组件实例无法释放",[15,584,585,520,587,524,589],{},[178,586,490],{},[59,588,523],{},[59,590,591],{},"bus.off",[19,593],{},[26,595,597],{"id":596},"_55-闭包引用大对象","5.5 闭包引用大对象",[15,599,600,229],{},[178,601,473],{},[52,603,606],{"className":604,"code":605,"language":209},[207],"Constructor    # Delta    Size Delta\n────────────────────────────────────\n(array)        +100       +10 MB\n(closure)      +50        +500 KB\n",[59,607,605],{"__ignoreMap":57},[15,609,610,612],{},[178,611,484],{},"：事件处理函数闭包不必要地引用了大对象",[15,614,615,617,618],{},[178,616,490],{},"：只保留必需的数据,或在使用后手动设为 ",[59,619,620],{},"null",[19,622],{},[11,624,626],{"id":625},"六实战案例排查-vue3-虚拟滚动内存泄漏","六、实战案例：排查 Vue3 虚拟滚动内存泄漏",[26,628,630],{"id":629},"_61-问题症状","6.1 问题症状",[15,632,633],{},"Performance 面板录制 60 秒后发现：",[30,635,636,639,642],{},[33,637,638],{},"JS Heap 从 50MB 增长到 150MB",[33,640,641],{},"内存呈阶梯状持续增长",[33,643,644],{},"没有明显的 GC 回收",[26,646,648],{"id":647},"_62-memory-面板对比快照","6.2 Memory 面板对比快照",[52,650,653],{"className":651,"code":652,"language":209},[207],"Comparison (Snapshot 1 vs Snapshot 2)：\n\nConstructor      # Delta    Size Delta\n────────────────────────────────────────\n(array)          +1        +50 MB      ← data 数组持续增长\nEventListener    +1        +100 KB     ← scroll 监听器未清理\nWebSocket        +1        +50 KB      ← WebSocket 未关闭\n",[59,654,652],{"__ignoreMap":57},[26,656,658],{"id":657},"_63-retainers-定位代码","6.3 Retainers 定位代码",[15,660,661],{},"通过查看 Retainers 路径,找到 3 个泄漏点：",[169,663,664,673,685],{},[33,665,666,229,669,672],{},[178,667,668],{},"data 数组",[59,670,671],{},"Window → VueComponent → setupState → data"," 持续增长,没有限制大小",[33,674,675,229,678,681,682],{},[178,676,677],{},"scroll 监听器",[59,679,680],{},"Window → eventListeners → scroll"," 未移除,闭包引用了 ",[59,683,684],{},"data",[33,686,687,690,691],{},[178,688,689],{},"WebSocket","：组件销毁时未关闭,回调闭包引用了 ",[59,692,684],{},[26,694,696],{"id":695},"_64-修复方案","6.4 修复方案",[15,698,699],{},"根据 Retainers 路径,需要：",[30,701,702,708,717,722],{},[33,703,704,705,707],{},"限制 ",[59,706,684],{}," 数组最大长度",[33,709,710,711,713,714,716],{},"在 ",[59,712,523],{}," 中移除 ",[59,715,454],{}," 监听器",[33,718,710,719,721],{},[59,720,523],{}," 中关闭 WebSocket 连接",[33,723,724,725,728],{},"使用 ",[59,726,727],{},"shallowRef"," 优化大数据响应式开销",[19,730],{},[11,732,734],{"id":733},"七快速检查清单","七、快速检查清单",[15,736,737,738,740],{},"Vue 3 组件中需要在 ",[59,739,523],{}," 清理的资源：",[169,742,743,754,762,769,778,786,795],{},[33,744,745,229,748,528,751],{},[178,746,747],{},"定时器",[59,749,750],{},"clearInterval(timer)",[59,752,753],{},"clearTimeout(timer)",[33,755,756,229,759],{},[178,757,758],{},"全局事件监听",[59,760,761],{},"window.removeEventListener('resize', handler)",[33,763,764,229,766],{},[178,765,689],{},[59,767,768],{},"ws.close()",[33,770,771,229,774,777],{},[178,772,773],{},"第三方库实例",[59,775,776],{},"chart.dispose()"," (如 ECharts)",[33,779,780,229,783],{},[178,781,782],{},"事件总线",[59,784,785],{},"bus.off('event', handler)",[33,787,788,229,791,794],{},[178,789,790],{},"Observer",[59,792,793],{},"observer.disconnect()"," (IntersectionObserver/ResizeObserver)",[33,796,797,800,801,803],{},[178,798,799],{},"大数据限制","：使用 ",[59,802,727],{}," + 限制数组最大长度",[19,805],{},[11,807,809],{"id":808},"八总结","八、总结",[26,811,812],{"id":812},"排查流程",[169,814,815,821,827,833,839],{},[33,816,817,820],{},[178,818,819],{},"Performance 面板"," → 确认是否泄漏（观察内存走势图）",[33,822,823,826],{},[178,824,825],{},"Memory 面板"," → 对比快照,找到泄漏对象类型",[33,828,829,832],{},[178,830,831],{},"查看 Retainers"," → 找到引用路径",[33,834,835,838],{},[178,836,837],{},"映射到代码"," → 通过变量名定位文件和行号",[33,840,841,844],{},[178,842,843],{},"修复 + 验证"," → 清理资源,再次录制确认修复",[26,846,847],{"id":847},"关键技巧",[30,849,850,856,862,868,874],{},[33,851,852,855],{},[178,853,854],{},"看 Retainers","：这是定位代码的关键,显示从 Window 到具体变量的完整路径",[33,857,858,861],{},[178,859,860],{},"认识常见模式","：定时器、事件监听器、DOM 引用、闭包是主要原因",[33,863,864,867],{},[178,865,866],{},"使用 shallowRef","：大数据场景必备,减少响应式开销",[33,869,870,873],{},[178,871,872],{},"限制数据量","：虚拟滚动中必须限制数组大小",[33,875,876,229,879,881],{},[178,877,878],{},"清理资源",[59,880,523],{}," 中清理所有副作用",[26,883,884],{"id":884},"最佳实践",[30,886,887,893,899,902,905],{},[33,888,889,890,892],{},"✅ 所有副作用都在 ",[59,891,523],{}," 中清理",[33,894,895,896,898],{},"✅ 使用 ",[59,897,727],{}," 存储大数据",[33,900,901],{},"✅ 限制列表/数组的最大长度",[33,903,904],{},"✅ 避免闭包捕获大对象",[33,906,907],{},"✅ 定期用 DevTools 检查内存占用",[19,909],{},[11,911,912],{"id":912},"参考资源",[30,914,915,924,931],{},[33,916,917],{},[918,919,923],"a",{"href":920,"rel":921},"https://developer.chrome.com/docs/devtools/memory-problems/",[922],"nofollow","Chrome DevTools 官方文档 - Memory",[33,925,926],{},[918,927,930],{"href":928,"rel":929},"https://vuejs.org/guide/best-practices/performance.html",[922],"Vue 3 性能优化指南",[33,932,933],{},[918,934,937],{"href":935,"rel":936},"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management",[922],"JavaScript 内存管理 - MDN",[939,940,941],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":57,"searchDepth":83,"depth":83,"links":943},[944,945,949,953,957,961,968,974,975,980],{"id":13,"depth":83,"text":13},{"id":23,"depth":83,"text":24,"children":946},[947,948],{"id":28,"depth":111,"text":28},{"id":47,"depth":111,"text":47},{"id":163,"depth":83,"text":164,"children":950},[951,952],{"id":167,"depth":111,"text":167},{"id":197,"depth":111,"text":197},{"id":242,"depth":83,"text":243,"children":954},[955,956],{"id":246,"depth":111,"text":246},{"id":360,"depth":111,"text":360},{"id":377,"depth":83,"text":378,"children":958},[959,960],{"id":381,"depth":111,"text":382},{"id":400,"depth":111,"text":401},{"id":460,"depth":83,"text":461,"children":962},[963,964,965,966,967],{"id":467,"depth":111,"text":468},{"id":499,"depth":111,"text":500},{"id":536,"depth":111,"text":537},{"id":566,"depth":111,"text":567},{"id":596,"depth":111,"text":597},{"id":625,"depth":83,"text":626,"children":969},[970,971,972,973],{"id":629,"depth":111,"text":630},{"id":647,"depth":111,"text":648},{"id":657,"depth":111,"text":658},{"id":695,"depth":111,"text":696},{"id":733,"depth":83,"text":734},{"id":808,"depth":83,"text":809,"children":976},[977,978,979],{"id":812,"depth":111,"text":812},{"id":847,"depth":111,"text":847},{"id":884,"depth":111,"text":884},{"id":912,"depth":83,"text":912},"2026-01-22T00:00:00.000Z","md","2026-01-22T08:35:33.662Z",{},true,"/post/performance/debugging-memory-leaks-with-chrome-devtools","---\ntitle: 使用 Chrome DevTools 排查内存泄漏\ndate: 2026-01-22\nlastmod: \"2026-01-22T08:35:33.662Z\"\n---\n\n\n\n## 概述\n\n内存泄漏是前端性能优化中最难排查的问题之一。本文将详细介绍如何使用 Chrome DevTools 的 Performance 和 Memory 面板，系统化地排查并定位内存泄漏问题，并映射到真实代码逻辑。\n\n***\n\n## 一、内存泄漏的识别\n\n### 典型症状\n\n* 页面运行一段时间后变卡\n\n* 滚动、点击响应变慢\n\n* 浏览器标签页显示内存占用持续增长\n\n* 最终页面崩溃（Out of Memory）\n\n### 快速检测\n\n在控制台运行以下代码,观察内存是否持续增长：\n\n```javascript\nsetInterval(() => {\n  const usedMB = performance.memory.usedJSHeapSize / 1024 / 1024\n  console.log(`当前内存占用: ${usedMB.toFixed(2)} MB`)\n}, 1000)\n```\n\n***\n\n## 二、Performance 面板：确认是否泄漏\n\n### 录制内存快照\n\n1. 打开 DevTools (F12) → Performance 标签\n2. 勾选 **Memory** 选项\n3. 点击 **Record** 录制 30-60 秒\n4. 执行可疑操作（滚动列表、打开关闭弹窗等）\n5. 停止录制\n\n### 分析内存走势图\n\n**正常情况**（有涨有跌,GC 能回收）：\n\n```\nMemory (MB)\n  ↑     ╱╲      ╱╲      ╱╲\n  │    ╱  ╲    ╱  ╲    ╱  ╲\n  └─────────────────────────→ 时间\n```\n\n**内存泄漏**（持续上涨,呈阶梯状）：\n\n```\nMemory (MB)\n  ↑   ╱──────╱─────╱─────╱──\n  │  ╱      ╱     ╱     ╱\n  └─────────────────────────→ 时间\n```\n\n**判断标准**：\n\n* ✅ 正常：内存有涨有跌,GC 后能降下来\n\n* ❌ 泄漏：内存持续上涨,GC 后仍然增长\n\n***\n\n## 三、Memory 面板：定位泄漏点\n\n### 对比堆快照\n\n**操作流程**：\n\n1. DevTools → Memory 标签 → 选择 \"Heap snapshot\"\n2. 点击 \"Take snapshot\" → 获得快照 1\n3. 执行可疑操作（如打开 10 次弹窗后关闭）\n4. 强制垃圾回收（点击 🗑️ 图标）\n5. 点击 \"Take snapshot\" → 获得快照 2\n6. 切换视图为 **Comparison** → 选择 \"between Snapshot 1 and Snapshot 2\"\n\n**关键列说明**：\n\n| 列名              | 含义       | 关注点              |\n| --------------- | -------- | ---------------- |\n| **# Delta**     | 净增加的对象数量 | 应该接近 0           |\n| **Size Delta**  | 净增加的内存   | **最关键的指标**       |\n| **Alloc. Size** | 新增对象占用内存 | 持续增长说明泄漏         |\n| **Freed Size**  | 释放的内存    | 应该接近 Alloc. Size |\n\n### 查找泄漏对象\n\n按 **Size Delta** 排序,找到占用内存最多的对象类型：\n\n```\nConstructor              # Delta    Size Delta\n─────────────────────────────────────────────\n(array)                  +500       +2.5 MB    ← 可疑！数组持续增长\nDetached HTMLDivElement  +200       +800 KB    ← DOM 泄漏\nEventListener            +150       +150 KB    ← 事件监听器未移除\n```\n\n***\n\n## 四、定位到真实代码\n\n### 查看 Retainers（保留路径）\n\n**这是最关键的环节！** Retainers 显示了为什么这个对象没有被垃圾回收。\n\n**操作**：点击可疑对象 → 选择具体实例 → 右侧面板显示 **Retainers**\n\n### 解读 Retainers 路径\n\n**示例 1：全局变量引用**\n\n```\nRetainers:\n  → Window / http://localhost:3000\n    → VueComponent                ← Vue 组件实例\n      → setupState                ← setup() 返回的状态\n        → allData                  ← 你的变量名\n          → @123456 (array)       ← 泄漏的数组\n```\n\n**如何对应到代码**：\n\n* 看到 `allData` → 在代码中搜索 `const allData = ref(...)`\n\n* 看到 `VueComponent` → 定位到具体的组件文件\n\n**示例 2：事件监听器引用**\n\n```\nRetainers:\n  → Window\n    → eventListeners          ← 全局事件监听器映射\n      → scroll                ← scroll 事件\n        → [[Handler]]\n          → VueComponent      ← 组件实例被闭包引用\n```\n\n**结论**：`scroll` 事件监听器没有被移除,闭包引用了组件实例。\n\n***\n\n## 五、常见内存泄漏模式\n\n通过 Memory 面板,常见的泄漏对象类型：\n\n### 5.1 DOM 泄漏\n\n**现象**：\n\n```\nConstructor              # Delta    Size Delta\n────────────────────────────────────────────\nDetached HTMLDivElement  +200       +800 KB\n```\n\n**原因**：移除 DOM 时未清理事件监听器,导致元素无法被 GC\n\n**解决**：在移除 DOM 前调用 `removeEventListener`\n\n***\n\n### 5.2 定时器泄漏\n\n**现象**：\n\n```\nConstructor    # Delta    Size Delta\n────────────────────────────────────\nTimeout        +50        +100 KB\n```\n\n**原因**：组件销毁时定时器仍在运行\n\n**解决**：在 `onUnmounted` 中调用 `clearInterval` / `clearTimeout`\n\n***\n\n### 5.3 事件监听器泄漏\n\n**现象**：\n\n```\nConstructor      # Delta    Size Delta\n──────────────────────────────────────\nEventListener    +100       +200 KB\n```\n\n**原因**：全局事件监听器未移除\n\n**解决**：在 `onUnmounted` 中调用 `window.removeEventListener`\n\n***\n\n### 5.4 组件实例泄漏\n\n**现象**：\n\n```\nConstructor        # Delta    Size Delta\n────────────────────────────────────────\nVueComponent       +50        +5 MB\n```\n\n**原因**：事件总线监听器未注销,导致组件实例无法释放\n\n**解决**：在 `onUnmounted` 中调用 `bus.off`\n\n***\n\n### 5.5 闭包引用大对象\n\n**现象**：\n\n```\nConstructor    # Delta    Size Delta\n────────────────────────────────────\n(array)        +100       +10 MB\n(closure)      +50        +500 KB\n```\n\n**原因**：事件处理函数闭包不必要地引用了大对象\n\n**解决**：只保留必需的数据,或在使用后手动设为 `null`\n\n***\n\n## 六、实战案例：排查 Vue3 虚拟滚动内存泄漏\n\n### 6.1 问题症状\n\nPerformance 面板录制 60 秒后发现：\n\n* JS Heap 从 50MB 增长到 150MB\n\n* 内存呈阶梯状持续增长\n\n* 没有明显的 GC 回收\n\n### 6.2 Memory 面板对比快照\n\n```\nComparison (Snapshot 1 vs Snapshot 2)：\n\nConstructor      # Delta    Size Delta\n────────────────────────────────────────\n(array)          +1        +50 MB      ← data 数组持续增长\nEventListener    +1        +100 KB     ← scroll 监听器未清理\nWebSocket        +1        +50 KB      ← WebSocket 未关闭\n```\n\n### 6.3 Retainers 定位代码\n\n通过查看 Retainers 路径,找到 3 个泄漏点：\n\n1. **data 数组**：`Window → VueComponent → setupState → data` 持续增长,没有限制大小\n2. **scroll 监听器**：`Window → eventListeners → scroll` 未移除,闭包引用了 `data`\n3. **WebSocket**：组件销毁时未关闭,回调闭包引用了 `data`\n\n### 6.4 修复方案\n\n根据 Retainers 路径,需要：\n\n* 限制 `data` 数组最大长度\n\n* 在 `onUnmounted` 中移除 `scroll` 监听器\n\n* 在 `onUnmounted` 中关闭 WebSocket 连接\n\n* 使用 `shallowRef` 优化大数据响应式开销\n\n***\n\n## 七、快速检查清单\n\nVue 3 组件中需要在 `onUnmounted` 清理的资源：\n\n1. **定时器**：`clearInterval(timer)` / `clearTimeout(timer)`\n2. **全局事件监听**：`window.removeEventListener('resize', handler)`\n3. **WebSocket**：`ws.close()`\n4. **第三方库实例**：`chart.dispose()` (如 ECharts)\n5. **事件总线**：`bus.off('event', handler)`\n6. **Observer**：`observer.disconnect()` (IntersectionObserver/ResizeObserver)\n7. **大数据限制**：使用 `shallowRef` + 限制数组最大长度\n\n***\n\n## 八、总结\n\n### 排查流程\n\n1. **Performance 面板** → 确认是否泄漏（观察内存走势图）\n2. **Memory 面板** → 对比快照,找到泄漏对象类型\n3. **查看 Retainers** → 找到引用路径\n4. **映射到代码** → 通过变量名定位文件和行号\n5. **修复 + 验证** → 清理资源,再次录制确认修复\n\n### 关键技巧\n\n* **看 Retainers**：这是定位代码的关键,显示从 Window 到具体变量的完整路径\n\n* **认识常见模式**：定时器、事件监听器、DOM 引用、闭包是主要原因\n\n* **使用 shallowRef**：大数据场景必备,减少响应式开销\n\n* **限制数据量**：虚拟滚动中必须限制数组大小\n\n* **清理资源**：`onUnmounted` 中清理所有副作用\n\n### 最佳实践\n\n* ✅ 所有副作用都在 `onUnmounted` 中清理\n\n* ✅ 使用 `shallowRef` 存储大数据\n\n* ✅ 限制列表/数组的最大长度\n\n* ✅ 避免闭包捕获大对象\n\n* ✅ 定期用 DevTools 检查内存占用\n\n***\n\n## 参考资源\n\n* [Chrome DevTools 官方文档 - Memory](https://developer.chrome.com/docs/devtools/memory-problems/)\n\n* [Vue 3 性能优化指南](https://vuejs.org/guide/best-practices/performance.html)\n\n* [JavaScript 内存管理 - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management)\n\n",{"title":5,"description":57},"post/performance/debugging-memory-leaks-with-chrome-devtools","TTh60MRKoaa6Ve3hr0KVgjYGW6YooqMFzXFd06wrGro",[992,996],{"title":993,"path":994,"stem":995},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":997,"path":998,"stem":999},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005085216]