不可解的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

相关推荐
yuanyxh1 小时前
Mac 软件推荐
前端·javascript·程序员
万少1 小时前
AtomCode开发微信小程序《谁去呀》 全流程
前端·javascript·后端
某人辛木1 小时前
Web自动化测试
前端·python·pycharm·pytest
Kagol2 小时前
Superpowers GSD gstack AgentSkills深度测评
前端·人工智能
excel3 小时前
JavaScript 字符串与模板字面量:从表象到本质理解
前端
京东云开发者3 小时前
当AI成为导演-如何用AI创作动漫短剧
前端
李白的天不白3 小时前
使用 SmartAdmin 进行前后端开发
java·前端
乘风gg4 小时前
🤡PUA AI Coding 工具 的 10 条终极语录
前端·ai编程·claude
学Linux的语莫4 小时前
Vue 3 入门教程
前端·javascript·vue.js
怕浪猫4 小时前
第一章、Chrome DevTools Protocol (CDP) 详解
前端·javascript·chrome