MutationObserver监听网页二次渲染和子节点变化

MutationObserver监听网页二次渲染和子节点变化

缘由

之前做性能优化的时候有一部分是关于对图片部分的优化 ,主要是做了懒加载 和借助工具对图片转码和压缩处理。但是事情做了,图片体积小了 加载时间短了 消耗的流量小了,但是怎么反馈优化结果,对比优化前后耗时和消耗流量的变化那,现有的前端监控工具一般对页面静态资源都是统计页面首次onload的数据,无法对二次动态加载资源到资源渲染结束做这种自由度较高的自定义统计。所以最后就决定借助MutationObserver自己统计数据 再用阿里云前端ARMS监控的api (window?.__bl?.sum和window?.__bl?.avg)自定义上报耗时均值和流量总量

一、MutationObserver 简介

MutationObserver 是浏览器原生提供的用于监听 DOM 结构变化的 API。它可以高效地监控节点的添加、删除、属性变化等,并在变化发生时异步通知开发者进行处理。相比早期的 DOM Mutation Events,MutationObserver 性能更优且不会阻塞主线程。

常见应用场景:

  • 动态内容加载(如图片、列表等)
  • 富文本编辑器
  • 组件化框架的 DOM 变更监听

二、基本用法

js 复制代码
// 创建一个新的 MutationObserver
const observer = new MutationObserver(() => {
  if (document.getElementById('xxx')) {
    // TODO: 此时开始加载第三方脚本
    observer.disconnect(); // 销毁监视者
  }
})

const config = { childList: true, subtree: true } // 对哪些更改做出反应

// 绑定目标节点并启动监视者
observer.observe(targetNode, config)

在完成对应逻辑后应该及时调用 observer.disconnect() 销毁监视者,否则第三方脚本里如果也操作了 DOM 就会不断递归。

config 对象有如下这些值,这些布尔选项表示会"对哪些更改做出反应":

kotlin 复制代码
• childList:监听子节点变动
• subtree:监听所有后代节点的变动
• attributes:监听节点的特性变化
• attributeFilter:特性名称数组,只观察选定的特性
• characterData:是否观察文本内容
• attributeOldValue:是否将特性的旧值和新值都传递给回调
• characterDataOldValue:是否将 node.data 的旧值和新值都传递给回调

二、项目应用

1 统计单次进入 商品点单页所有商品图片在页面上完全加载总耗时即页面完全加载时间
2 统计单次进入页面所有图片消耗的流量总值

首先创建一个 MutationObserver实例监听页面外层元素targetNode下元素变化 并启动监听者

javascript 复制代码
    const targetNode = document.querySelector(".page-cont");
      // 创建一个新的 MutationObserver
      const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          findImg(mutation.addedNodes);
        });
      });
      const config = {
        childList: true,
        subtree: true,
        //  attributes:true
      }; // 对哪些更改做出反应
      // 绑定目标节点并启动监视者
      observer.observe(targetNode, config);

然后 定义一个方法findImg 循环处理变化的子元素 通过node.nodeName == "IMG"找到动态渲染的图片元素 在通过imgnode.addEventListener("load", () => {xxx})判断图片dom完全渲染结束 通过performance.getEntriesByName Api解析图片尺寸

计算最后一个变化的img 元素加载完成或者失败, 即触发findImg次数等于图片加载成功imgnode.addEventListener("load", () => {xxx})和图片加载失败imgnode.addEventListener("error", () => {xxx})的次数总和的时候 就是所有商品图片在页面上完全加载总耗时即页面完全加载时间time 然后调用 前端监控埋点 window?.__bl?.avg( `shopLoadingTime_${this.$store.state.storeInfo.storeName}`,time);上报均值

同理最后一个变化的img 元素加载完成或者失败后 累加所有图片的流量值 然后然后调用 前端监控埋点 window?.__bl?.sum( `shopLoadingTime_${this.$store.state.storeInfo.storeName}`,sizes);上报总值

ini 复制代码
   let findImg = (list) => {
        for (const node of list) {
          if (node.nodeName == "IMG") {
            count++;
            node.addEventListener("load", () => {
              const url = node.currentSrc || node.src;
              if (url && url.length > 0) {
                // const canvas = document.createElement("canvas");
                // const context = canvas.getContext("2d");
                // // const img = document.getElementById("tulip");
                // context.drawImage(node, 0, 0);
                // const blob =  canvas.toBlob(blob => blob).then(blod=>{
                //   debugger
                // })

                const iTime = performance.getEntriesByName(url)[0];
                let cusize = Math.floor(iTime.transferSize / 1000);
                // console.log("當前圖片大小" + cusize + "kb");
                if (Number(cusize) > 1) {
                  sizes = sizes + cusize;
                }


              }
              setTimeout(() => {
                // console.log("1successfull loading image");
                count--;
                if (count == 0) {
                  let lodaingEndTime = new Date().getTime();
                  let loadingStartTime = Number(
                    window.localStorage.getItem("loadingStartTime")
                  );
                  delete localStorage["loadingStartTime"];
                  let time = (
                    (lodaingEndTime - loadingStartTime - 1000) /
                    1000
                  ).toFixed(2);
                  // console.log(
                  //   "all Image loaded successfull" + time + "s" + sizes + "kb"
                  // );

                  time < 200 &&
                    time != 0 &&
                    window?.__bl?.avg("shopLoadingTimeAllStore", time);
                  sizes != 0 &&
                    window?.__bl?.avg("shopAvgImgSizesAllStore", sizes);
                  if (this.$store?.state?.storeInfo?.storeName) {
                    time < 200 &&
                      time != 0 &&
                      window?.__bl?.avg(
                        `shopLoadingTime_${this.$store.state.storeInfo.storeName}`,
                        time
                      );
                    sizes != 0 &&
                      window?.__bl?.avg(
                        `shopAvgImgSizes_${this.$store.state.storeInfo.storeName}`,
                        sizes
                      );
                  }

                  sizes != 0 && window?.__bl?.sum("imgSizes", sizes);
                  observer.disconnect();
                }
              }, 500);
            });
相关推荐
weifont20 分钟前
聊一聊Electron中Chromium多进程架构
javascript·架构·electron
大得36924 分钟前
electron结合vue,直接访问静态文件如何跳转访问路径
javascript·vue.js·electron
水银嘻嘻2 小时前
12 web 自动化之基于关键字+数据驱动-反射自动化框架搭建
运维·前端·自动化
it_remember2 小时前
新建一个reactnative 0.72.0的项目
javascript·react native·react.js
小嘟嚷ovo3 小时前
h5,原生html,echarts关系网实现
前端·html·echarts
十一吖i3 小时前
Vue3项目使用ElDrawer后select方法不生效
前端
只可远观3 小时前
Flutter目录结构介绍、入口、Widget、Center组件、Text组件、MaterialApp组件、Scaffold组件
前端·flutter
周胡杰3 小时前
组件导航 (HMRouter)+flutter项目搭建-混合开发+分栏效果
前端·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
敲代码的小吉米4 小时前
前端上传el-upload、原生input本地文件pdf格式(纯前端预览本地文件不走后端接口)
前端·javascript·pdf·状态模式
是千千千熠啊4 小时前
vue使用Fabric和pdfjs完成合同签章及批注
前端·vue.js