html 清除dom节点后占用的清除内存未释放
页面越用越卡,内存曲线像爬坡一样只升不降。点后的内我排查过不少这样的占用案例,最后都指向同一个凶手:你删掉了DOM节点,释放但浏览器没把内存还给你。清除这不是点后的内浏览器的bug,而是占用你留下的引用让垃圾回收机制无从下手。
举个例子。释放你有个弹窗组件,清除关闭时执行了 modal.remove(),点后的内节点确实从页面消失了。占用但如果你在某个全局变量里存过这个modal的释放引用——哪怕是存在一个已清空的数组里,只要数组本身还在——那这个DOM连同它下面挂载的清除事件监听器、绑定的点后的内数据,全都被“保鲜”在内存里。占用我见过最夸张的一次,一个后台管理系统反复打开关闭弹窗,半小时后标签页占用了1.2GB内存。打开任务管理器一看,页面进程比视频播放器还吃资源。
更隐蔽的是事件委托的坑。很多人习惯把监听器绑在document上,通过判断target来响应。这本身没问题,但如果你在移除节点时忘了移除它自身的 onclick 或 addEventListener 注册的独立监听器——尤其当监听器是匿名函数时——这个节点就成了孤岛。DOM树里找不到它,但事件系统里还拽着它。垃圾回收器扫描时发现“这玩意儿还被事件系统引用着”,好,那你继续活着吧。
还有个常被忽略的场景:iframe。动态创建一个iframe,用完就 remove(),看起来干净利落。但iframe内部的window对象如果被外部变量引用过,整个iframe的文档环境都释放不掉。我有次做了一个富文本编辑器,每次打开都会new一个iframe,关闭时只移除DOM。结果打开十几次后,页面直接崩溃。后来改成复用同一个iframe,问题才消失。
怎么破?三个土办法,亲测有效。
第一,移除节点前,主动解绑。别依赖 remove() 帮你清理一切。手动把节点上的事件监听器置空:node.onclick = null,或者用 removeEventListener 把具名函数摘掉。匿名函数没法摘?那就别用匿名函数,定义个具名函数再绑。
第二,用WeakMap或WeakSet存DOM引用。这俩结构的特点是“弱引用”——只要没有其他强引用,垃圾回收器随时可以收走。你存了弹窗节点,弹窗关了,节点没别的引用,内存立刻释放。注意,别用普通Object或Map去存,那会变成强引用,反而帮倒忙。
第三,清空容器时,先断开子节点与外部的一切联系。比如你有个列表容器,要重置内容,别直接 container.innerHTML = '',这招虽然快,但如果子节点里有绑定过自定义属性的对象——比如 element._data = someObject——那someObject也被连带锁住。正确姿势是先遍历子节点,把 _data 这类自定义属性删掉,再清空innerHTML。
最后说个反直觉的事:有时候你什么都不做,内存反而会自动释放。浏览器的垃圾回收机制比你想象的聪明,它会在页面空闲时做全量扫描。但问题在于——用户不会等你空闲。他们切换标签页、滚动页面、点击按钮,这些操作都会打断GC的节奏。所以别把希望全寄托在浏览器身上,你自己管好引用,才是根治之道。
写代码时多问一句:这个DOM死了之后,还有谁在惦记它?没人惦记,它才走得安详。
