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);
});