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);
            });
相关推荐
mldong1 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60612 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角3 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!3 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi5 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu7 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss