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

相关推荐
提笔了无痕3 小时前
Web中Token验证如何实现(go语言)
前端·go·json·restful
戌中横3 小时前
JavaScript——Web APIs DOM
前端·javascript·html
Beginner x_u3 小时前
如何解释JavaScript 中 this 的值?
开发语言·前端·javascript·this 指针
HWL56794 小时前
获取网页首屏加载时间
前端·javascript·vue.js
烟锁池塘柳04 小时前
【已解决】Google Chrome 浏览器报错 STATUS_ACCESS_VIOLATION 的解决方案
前端·chrome
速易达网络4 小时前
基于RuoYi-Vue 框架美妆系统
前端·javascript·vue.js
LYS_06184 小时前
RM赛事C型板九轴IMU解算(4)(卡尔曼滤波)
c语言·开发语言·前端·卡尔曼滤波
We་ct5 小时前
LeetCode 151. 反转字符串中的单词:两种解法深度剖析
前端·算法·leetcode·typescript
yinmaisoft5 小时前
JNPF 表单模板实操:高效复用表单设计指南
前端·javascript·html
37方寸6 小时前
前端基础知识(JavaScript)
开发语言·前端·javascript