不可解的Dom泄漏问题,Dom泄漏比你预期得更严重和普遍

先说一下结论:内存泄漏是前端性能优化中的常见问题,而Dom泄漏则是内存泄漏的重要途径和方式,但是在业务中的大多数场景下,Dom泄漏其实几乎是一个不可解问题,或者说你可能需要花费大量的精力去维护一个脆弱不堪的系统

可以先看一下这篇很好的文章使用Chrome开发者工具分析内存泄漏 ,初步学习一下浏览器的内存泄漏面板🍞如何使用以及内存泄漏的相关术语

简单的Demo

试一下下面这个简单demo

打开内存分析面板,然后执行如下操作,点击创建列表---创建并泄漏一个与列表节点----卸载子树。

你就会发现,你仅仅只是不小心泄漏了一个不在列表中的简单节点,却最终会导致整个子树的全部泄漏,并且点几次全部泄漏几次

内存分析面板,其中的Detach div代表着不在dom树🌲中的dom节点,也就是泄漏的dom节点

部分构造函数说明
(compiled code) --- 编译代码占用Detached - div 不处于dom树内的dom节点,代表泄漏的dom,是内存分析的重点
system / Context调用一个函数所需要的潜在对象,例如闭包使用的实际数据InternalNode此类别表示在 V8 之外分配的对象,例如 Blink 定义的 C++ 对象。
(closure) --- 闭包(array)
Object --- JS对象类型(system)
(string) --- 字符串类型,有时对象里添加了新属性,属性的名称也会出现在这里
Array --- JS数组类型cls Window --- JS的window对象

Tabs/Route 灾难

上面那个例子🌰可能不够震撼,来看看这个

业务中常见的tabs切换,稍微点几次切换,打开堆内存快照

JSX 复制代码
import { Tabs } from "antd";
import React, { useEffect, useRef } from "react";

import { Avatar, List } from "antd";

const dataBase = [
  {
    title: "Ant Design Title 1",
  },
  {
    title: "Ant Design Title 2",
  },
  {
    title: "Ant Design Title 3",
  },
  {
    title: "Ant Design Title 4",
  },
];

const data = new Array(20).fill(0).reduce((pre, now) => {
  return [...pre, ...dataBase];
}, [] as any[]);

const ListDemo: React.FC = () => {
  const ref = useRef<HTMLAnchorElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (el) {
      //模拟意外泄漏
      document.addEventListener("visibilitychange", (e) => {
        if (el && document.visibilityState === "visible") {
          el.classList.add("active");
        }
      });
    }
  }, []);

  useEffect(() => {
    //do something
    return () => {
      //clear
    };
  });
  return (
    <div>
      <List
        itemLayout="horizontal"
        dataSource={data}
        renderItem={(item: any, index) => {
          return (
            <List.Item>
              <List.Item.Meta
                avatar={
                  <Avatar
                    src={`https://api.dicebear.com/7.x/miniavs/svg?seed=${index}`}
                  />
                }
                title={<a href="https://ant.design">{item.title}</a>}
                description="Ant Design, a design language for background applications, is refined by Ant UED Team"
              />
            </List.Item>
          );
        }}
      />
      <div>
        <div>
          <a href="" className="leak-dom" ref={ref}>
            泄漏dom
          </a>
        </div>
      </div>
    </div>
  );
};

//page leak
const App: React.FC = () => (
  <Tabs defaultActiveKey="1" destroyInactiveTabPane>
    <Tabs.TabPane tab="Tab 1" key="1">
      <ListDemo />
    </Tabs.TabPane>
    <Tabs.TabPane tab="Tab 2" key="2">
      <ListDemo />
    </Tabs.TabPane>
    <Tabs.TabPane tab="Tab 3" key="3">
      <ListDemo />
    </Tabs.TabPane>
  </Tabs>
);

export default App;

这dom啊,你一点,诶---------它全部漏了,pane级别的哦😯

实际生产环境的例子🌰

上面的举例可能有些强行,我们🉑一看看被带到实际线上环境的例子🌰

打开掘金创作者中心 juejin.cn/creator/gro... 在数据中心,创作成长等tabs页随意切换多次

录制快照,可以在录制⏺️之前点击几次垃圾回收♻️

此处可以@掘金官方,你也可以去查看任意网站的会destroy的Tabs上去试试,大概率都会复刻

为什么会这样?

原理其实很简单,任何一个dom上都存在父,子,前后等一长串的引用链,任意一个微小的dom节点就可以catch住整个子树🌲(e.target也同理)

如何解决?

根据上述描述,应该可以清楚这根本不是一个可解的问题,或许你花了大量的精力去解决了现在的Dom泄漏问题,但后续的任意一个小迭代都能让你的所有成果付之一炬,甚至有些问题是第三方库带来的,根本无从下手。我们只能从某些方面去尽量避免它

非必要不删除

既然被删除的dom几乎一定会被泄漏,那么直接不删除就行,比如Tabs存在高频切换的场景就不要destroy

保持对dom的间接引用

通过useRef或document.querySelector的间接引用机制,永远防止直接创建对dom的引用路径(不过这种方式更多是一种编程习惯,解决不了上述问题)

JSX 复制代码
  useEffect(() => {
    if (ref.current) {
      //el的简写写在回调函数内,不要写在外部,保持对dom的间接引用,这样稳定不会发生dom泄漏
      const el = ref.current;
      document.body.addEventListener("click", (e) => {
       //const el = ref.current;
        if (el) {
          console.log(el.id);
        }
      });
    }
  }, []);

querySelector形式

JSX 复制代码
  useEffect(() => {
    if (ref.current) {
      //模拟意外泄漏
      document.body.addEventListener("click", (e) => {
        //querySelector保持间接引用
        const el = document.querySelector(".el");
        console.log(el?.classList);
      });
    }
  }, []);

Iframe内存隔离

预览页等大型Dom的Drawer或Modal,无法采用不摧毁方案,每次关闭-新建都会导致页面级Dom泄漏,由于内存泄漏原理是变量具有从全局对象到变量的路径,因此通过Iframe的隔离方案,实际上也能解决内存隔离的问题

javascript 复制代码

参考文档

参考文档

使用Chrome开发者工具分析内存泄漏

内存泄漏术语

内存术语

录制堆快照

解决内存问题

MemLab

内存面板

Example 9: DOM leaks bigger than expected

相关推荐
—Qeyser几秒前
用Deepseek写扫雷uniapp小游戏
开发语言·前端·javascript
摆烂工程师27 分钟前
保姆教程:2025年 ChatGPT Plus 的订阅升级攻略和支付失败的解决方式
前端·后端·程序员
蓝屏的钙35 分钟前
H5 如何 C 端唤起导航高德百度 APP
前端·app
冴羽1 小时前
SvelteKit 最新中文文档教程(13)—— Hooks
前端·javascript·svelte
skywalk81631 小时前
自动化浏览器的测试框架playwright 支持多种浏览器Chromium、Firefox 和 WebKit
前端·chrome·自动化·测试·playwright
小宋10211 小时前
Vue实现动态数据透视表(交叉表)
前端·javascript·vue.js·数据透视表·动态交叉表
x-cmd1 小时前
x-cmd install | Wuzz - Web 开发与安全测试利器,交互式 HTTP 工具
前端·网络协议·安全·http·浏览器·测试·命令行
羊思茗5201 小时前
CSS3:深度解析与实战应用
前端·css·css3
天下代码客1 小时前
【八股】未知宽高元素水平垂直居中的三种方法
javascript·css·html
无名之逆1 小时前
[特殊字符] Hyperlane:Rust 高性能 Web 框架的终极选择 [特殊字符]
服务器·开发语言·前端·网络·后端·http·rust