前端工程化—— 埋点&监控

埋点

“埋点” 是一种在应用程序或网站中插入代码的技术,用于收集用户行为数据或特定事件的信息。它是用于分析和监控用户行为、应用性能和其他关键指标的一种常用方法。通过在特定位置插入代码或调用特定的 API,开发人员可以捕获有关用户如何与应用程序或网站交互的数据。

埋点的目的是为了收集关键的指标和数据,以便帮助了解用户行为、改进用户体验、优化应用性能、进行 A/B 测试和支持业务决策。通过埋点,您可以收集以下类型的数据:

  1. 用户行为数据:例如页面浏览量、点击事件、表单提交、购买行为等。
  2. 应用性能数据:例如页面加载时间、API 调用延迟、错误日志等。
  3. 设备和环境数据:例如用户设备类型、操作系统、浏览器版本等。
  4. 用户属性数据:例如用户ID、地理位置、用户角色等。

常见的埋点方式包括:

  1. 手动埋点:开发人员在代码中显式地插入埋点代码,通常使用 JavaScript 或其他编程语言实现。
  2. 自动埋点:使用自动化工具或框架,自动收集某些标准事件或用户行为数据。
  3. 可视化埋点:使用可视化工具,在页面上直接选择元素或交互,并配置要捕获的事件。

埋点数据通常会被发送到数据分析平台或服务,如Google Analytics、Mixpanel、Amplitude、Heap等,用于处理和分析数据。分析人员和业务决策者可以使用这些数据来获得深入了解用户行为和应用性能的见解,以便优化产品和业务策略。

IntersectionObserver

在 JavaScript 中,IntersectionObserver 是一个构造函数,可以创建一个新的观察者对象,用于观察目标元素与其包含父元素或视口之间的交叉区域。

前端工程化—— 埋点&监控

IntersectionObserver 对象具有以下属性:

  • root: 表示目标元素所在的根元素。它是一个 DOM 节点,默认为 null,表示使用视口作为根元素。可以设置这个属性来观察目标元素与其包含父元素之间的交叉区域。
  • rootMargin: 一个用于扩展或缩小 root 元素边界的 margin 值,用于扩大或缩小交叉区域的边界。如果在初始化的时候未被指定,它将被设置成默认值 "0px 0px 0px 0px"
  • thresholds: 一个数组,表示当目标元素与根元素的交叉区域达到指定的阈值时,触发回调函数。阈值是一个0.0到1.0的数字数组,默认为 [0],表示目标元素进入或离开根元素时立即触发回调。
  • activeObservations: 一个 IntersectionObserverEntry 对象数组,表示当前正在被观察的元素的交叉信息。
  • constructor: 一个表示 IntersectionObserver 构造函数的引用。

IntersectionObserver 对象通常在实例化时使用这些属性进行配置,然后通过调用 observe() 方法观察指定的目标元素。

const options = {
  root: document.querySelector('#container'),
  rootMargin: '50px',
  threshold: [0, 0.5, 1]
};

const observer = new IntersectionObserver(callback, options);

function callback(entries, observer) {
  entries.forEach(entry => {
    console.log(entry.intersectionRatio); // 交叉区域的比例
    console.log(entry.isIntersecting); // 是否与根元素交叉
    console.log(entry.target); // 目标元素本身
  });
}

const targetElement = document.querySelector('#target');
observer.observe(targetElement);

上述代码创建了一个 IntersectionObserver 实例,并将其配置为观察一个名为 targetElement 的元素。当该元素进入或离开其父元素,即root设置的对象: #container 的交叉区域时,触发回调函数 callback。回调函数中的 entries 参数是一个包含 IntersectionObserverEntry 对象的数组,提供了关于交叉区域的信息,如交叉比例、是否交叉等。

IntersectionObserverEntry

IntersectionObserverEntry 对象是由 IntersectionObserver 回调函数传递的观察者的条目信息。它提供了有关目标元素与其根元素或视口之间交叉区域的详细信息。当目标元素进入或离开根元素或视口的交叉区域时,会创建一个新的 IntersectionObserverEntry 对象,并传递给观察者的回调函数。

IntersectionObserverEntry 对象具有以下属性:

  • time: 表示交叉区域变化发生的时间戳,以毫秒为单位。
  • rootBounds: 一个 DOMRectReadOnly 对象,表示根元素的边界框信息。
  • boundingClientRect: 一个 DOMRectReadOnly 对象,表示目标元素的边界框信息(相对于视口)。
  • intersectionRect: 一个 DOMRectReadOnly 对象,表示目标元素与根元素的交叉区域的边界框信息。
  • intersectionRatio: 一个表示交叉区域的比例的值,范围从 0.0(目标元素完全在根元素之外)到 1.0(目标元素完全在根元素之内)。
  • isIntersecting: 一个布尔值,表示目标元素当前是否与根元素交叉。如果目标元素至少有一个像素与根元素交叉,则为 true,否则为 false
  • target: 表示被观察的目标元素本身。

在观察者的回调函数中,可以通过 IntersectionObserverEntry 对象的这些属性来了解目标元素与根元素的交叉情况,并根据需要执行相应的操作。

const options = {
  threshold: [0, 0.5, 1]
};

const observer = new IntersectionObserver(callback, options);
//entries即为IntersectionObserverEntry对象
function callback(entries, observer) {
  entries.forEach(entry => {
    console.log('IntersectionRatio:', entry.intersectionRatio);
    console.log('Is Intersecting:', entry.isIntersecting);
    console.log('Target element:', entry.target);
    console.log('Root bounds:', entry.rootBounds);
    console.log('Bounding client rect:', entry.boundingClientRect);
    console.log('Intersection rect:', entry.intersectionRect);
    console.log('----------------------');
  });
}

const targetElement = document.querySelector('#target');
observer.observe(targetElement);

每当 targetElement 进入或离开视口的交叉区域时,回调函数会被触发,并打印 IntersectionObserverEntry 对象的相关属性信息。

getBoundingClientRect

getBoundingClientRect 是一个 DOM 元素的方法,用于获取该元素相对于视口(viewport)的位置信息。它返回一个 DOMRect 对象,其中包含了元素的位置、大小等属性。

DOMRect 对象包含以下属性:

  • top: 元素上边界相对于视口顶部的距离。
  • right: 元素右边界相对于视口左边的距离。
  • bottom: 元素下边界相对于视口顶部的距离。
  • left: 元素左边界相对于视口左边的距离。
  • width: 元素的宽度。
  • height: 元素的高度。

使用 getBoundingClientRect 可以用来计算元素在视口中的位置,判断元素是否可见,实现动态布局等等。

IntersectionObserver源码

下面的代码是一个简化版本的示例,并非完整的 IntersectionObserver 源代码。但是也说明 了IntersectionObserver 的基本工作原理。IntersectionObserver 是由浏览器实现并内置的功能,不需要开发者手动实现。

// IntersectionObserver 构造函数
function IntersectionObserver(callback, options) {
this.callback = callback;
this.options = options;
this.targets = []; //观察的目标数组
// 监听视口的滚动事件
window.addEventListener('scroll', this.handleScroll.bind(this));
}
// 监听目标元素是否进入或离开视口的方法 observe方法
IntersectionObserver.prototype.observe = function (target) {
if (this.targets.indexOf(target) === -1) {//不存在该观察的元素,push进target数组
this.targets.push(target);
}
this.handleScroll(); // 立即检查目标元素的状态
};
// 处理视口滚动事件
IntersectionObserver.prototype.handleScroll = function () {
const viewportHeight = window.innerHeight; //当前仅视为相对于window,root为null
// 遍历所有观察目标元素
this.targets.forEach(target => {
//利用该getBoundingClientRect的api
const targetRect = target.getBoundingClientRect();
//options设置的margin参数
const rootMargin = this.options.rootMargin || '0px';
// 计算目标元素的可见区域
const isVisible = targetRect.top - parseInt(rootMargin) < viewportHeight && targetRect.bottom + parseInt(rootMargin) > 0;
// 创建 IntersectionObserverEntry 对象
const entry = {
time: Date.now(),
target: target,
rootBounds: null,
boundingClientRect: targetRect,
intersectionRect: isVisible
? {
top: Math.max(targetRect.top, 0),
bottom: Math.min(targetRect.bottom, viewportHeight),
left: 0,
right: window.innerWidth,
width: window.innerWidth,
height: Math.min(targetRect.height, viewportHeight)
}
: { top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0 },
intersectionRatio: isVisible
? (Math.min(targetRect.bottom, viewportHeight) -
Math.max(targetRect.top, 0)) /
targetRect.height
: 0,
isIntersecting: isVisible
};
// 调用回调函数,并传递 IntersectionObserverEntry 对象
this.callback([entry], this);
});
};
// 创建一个 IntersectionObserver 实例
const observer = new IntersectionObserver(function (entries, observer) {
// 在此处执行交叉区域变化的逻辑
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Element is visible');
} else {
console.log('Element is not visible');
}
});
}, {
root: null, // 使用视口作为根元素
threshold: 0.5 // 50% 的交叉区域时触发回调
});
// 观察目标元素
const targetElement = document.querySelector('#target');
observer.observe(targetElement);

以上只是 IntersectionObserver 的简化版本,实际的 IntersectionObserver API 在浏览器中进行了更复杂和高效的实现。这个示例仅用于展示 IntersectionObserver 的基本工作原理和用法。在实际使用中,无需手动实现 IntersectionObserver,只需要使用浏览器内置的 API 即可。

方式

埋点数据的上报方式可以有多种形式,具体选择哪种方式取决于您的应用程序和需求。以下是一些常见的埋点数据上报方式:

  1. HTTP请求: 埋点数据可以通过发送HTTP请求将收集到的数据上报到后端服务器或第三方统计平台。通常使用POST请求将数据作为参数发送到指定的接口。后端服务器或第三方平台接收数据后进行处理和存储。
  2. WebSocket: 如果您需要实时上报数据或持续与后端保持连接,可以使用WebSocket协议。WebSocket允许客户端与服务器进行双向通信,可以实时发送埋点数据到后端。
  3. 日志文件: 埋点数据可以以日志文件的形式记录在客户端或服务器上,然后通过定时任务或其他手段将日志文件上传到后端进行处理。这种方式适用于离线处理和批量上报数据。
  4. 消息队列: 使用消息队列可以实现异步上报数据,将埋点数据放入消息队列,然后由后台服务从消息队列中取出数据进行处理和上报。
  5. 可视化埋点工具: 一些可视化埋点工具提供了自动上报功能,它们会在前端代码中自动插入埋点代码,并将收集到的数据上报到它们的服务器。您只需要在工具的配置界面定义需要追踪的事件,工具会自动生成并上报相应的数据。
  6. 第三方SDK: 有些第三方埋点服务提供SDK,您只需要集成他们的SDK到您的应用程序中,SDK会负责收集和上报数据。

不论您选择哪种方式,都需要考虑数据的安全性、实时性和准确性。同时,还需注意数据量的大小和频率,避免因过多的上报数据而影响应用程序性能或网络负载。定期监控和分析上报的数据,确保收集到有用的信息,为产品优化和决策提供支持。

步骤:
如何触发埋点上报事件: 曝光 等

获取页面信息

避免重复上报,去重

上报

legoInit

export type LegoInitParamsType = {
log: boolean //日志
t?: string
routeList?: RouteListType[]  //路由
name?: string
pagequery?: string
pageIdInfo?: any //页面信息getPageIdInfo 从dom上获取zz-pageinfo的属性
forbidLegoInPrerender?: any // 设置为body.forbidLegoInPrerender
forbidLegoInHidden?: any // body.forbidLegoInHidden
stayUserTime?: boolean
subpageID?: string
debugger?: boolean  //处理调试工具  初始化legoInitLite对象的时候变为true
mchannel?: string
setCloseMark?: boolean //
debuggerCache?: boolean //处理调试工具  初始化legoInitLite对象的时候变为true
autoGoodsExpose?: boolean // startMutationObserver
autoAreaExpose?: boolean // startMutationObserver
autoOcd?: boolean    //初始化legoInitLite对象的时候变为true
}

初始化legoInitLite对象

  • 处理调试工具,初始化全局参数
  • 设置t值

含义:cookie中的t值,表示运行当前M页的容器(例如转转iOS、转转安卓、58APP、微信等),其中不同小程序拥有不同t值,兼容处理从userAgent进行判断.小程序则必须为每一个在 webview 中使用的页面链接的 url 上自动封装 __t 参数,保证小程序中的 M 页也可以正确的埋入 t 值;

  • t值取值逻辑
    • 首先根据调用方法传入的t来设置,先取参数的值
    • 再从顶层窗口 url 上去 t (iframe)
    • 再从当前 url 参数中取
    • 最后从ua中取
const t = __t || getQueryFromTop('__t') || getQuery('__t') || getTFromUA()
  • 设置tk值,一般是客户端注入的,如果没有就是nginx层设置的

    tk即为token,取值方法

    • 首先cookies里面取tk, cookies里面的是客户端埋入
    • 如果cookies里面没有从url上取tk, 小程序环境中,则只有在url上有tk
    • 如果还没有,使用idzz或者id58
  • 设置 referer 的值,即设定 pageUrl 就是 referer,将其设置为zzreferer存在cookie当中。initReferer()函数当中进行设置,setCookie({ zzreferer: pageUrl })

  • 过滤中 pageuqery 无效值, 防止 url 过长

  • 把指定的参数统一放到window[‘zz_lego_backup’]中

  • 初始化使用插件,挂载use

startMutationObserver 开启MutationObserver

两个参数,autoGoodsExpose autoAreaExpose 分别开启对应的商品曝光和区域曝光

观察根节点的子孙节点新增和删除

MutationObserver

MutationObserver 是 JavaScript 中的一个 Web API,它允许开发者监视 DOM 树的变化。通过 MutationObserver,可以监听指定 DOM 元素及其子元素的增加、删除、属性变化等事件,从而在 DOM 发生变化时执行特定的操作或逻辑。

使用 MutationObserver 的基本步骤如下:

  1. 创建一个 MutationObserver 实例,并传入一个回调函数作为参数。该回调函数会在 DOM 发生变化时被调用。
  2. 使用 observe() 方法指定要监听的 DOM 元素及其配置选项。配置选项包括是否监听子元素的变化、是否监听属性变化等。
  3. 在回调函数中处理 DOM 变化时的逻辑。
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<div id="container">
<p>Example</p>
</div>
<script>
// 创建 MutationObserver 实例并定义回调函数
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('子元素发生变化!');
} else if (mutation.type === 'attributes') {
console.log('元素属性发生变化!');
}
}
});
// 监听指定 DOM 元素及其子元素的增加、删除、属性变化
observer.observe(document.getElementById('container'), {
childList: true,
attributes: true,
subtree: true
});
// 模拟 DOM 变化
setTimeout(() => {
document.getElementById('container').appendChild(document.createElement('p'));
}, 2000);
</script>
</body>
</html>

在上述示例中,创建了一个 MutationObserver 实例,并定义了回调函数。通过 observer.observe() 方法,我们监听 id"container"<div> 元素及其子元素的增加、删除和属性变化。

然后,通过 setTimeout 模拟了一个 DOM 变化,向 id"container"<div> 元素中添加了一个新的 <p> 元素。,MutationObserver 监测目标节点及其子孙节点的添加、删除、属性变化等操作。当 DOM 发生变化时,回调函数会输出相应的提示信息。

visitOrderTracking 自动埋点逻辑初始化

  • zpm逻辑初始化,依赖入参数 routeList,即项目里的pageIdConfig.js文件, 存储pageId信息
   // pageIdInfoArr
[{     name: 'order-detail',     route: '/order/detail',     pageId: 'K1083',     level: 6   },    {      name: 'external',      route: '/helpsale/external',      pageId: 'M6670',      level: 2    }]
  • 初始化点击事件监听 initEventListener,页面点击监听,收集参数,sortId,sortName等
    document.addEventListener('click', clickCallback, true)
    

clickCallback

  • sectionId 是当前页面区域ID对页面中区域划分唯一标识,所以该节点需要存在ZZ_SECTION_ID属性,否则不能进行下一步查找。
 if (document.querySelector) {
const ele = document.querySelector(`[${ZZ_SECTION_ID}]`)
if (!ele) return
}

clickCallback核心:

递归算法,

  • 获取 sectionId,先获取父节点的,没有则获取自身的,二者如若存其一,则结束

  • 赋值父节点为父父级节点, parent = parent.parentElement,继续获取

  • 一直刀没有父节点或者整个 dom 树没有 sectionId 则退出

查找顺序 父级 –> 自身 –> 父父级

开启页面zpmshow事件插件

visitStackTracking 初始化页面展现钩子逻辑

初始化页面展现钩子,记录页面的访问路径根据

  • cookie当中是否有zzVisitStack,有就赋值给visitStack,访问栈的最大长度stackMaxLen = 2
  • 判断visitStack当前长度
    • 为0,push当前路由
    • 不为0,比对当前路由与上一次记录的路由是否相同,进行更新访问栈的操作
      • 不同,push操作
      • 相同,更新并替换访问事件
  • visitStack长度是否超出最大长度,超出了,留最新访问的两个记录,即最后两条
  • 设置cookie为 zzVisitStack

其余操作

1、开启统计页面停留时长功能,
2、把两个函数分别添加到生命周期onShow、onHide当中,利用performance对象,保存startTime = performance.now()

  • onShow当中的函数设置setInterval,定时计算elapsedTime = ((performance.now() – startTime) / 1000).toFixed(2),并与其他数据进行整合,存储本地localStorage.setItem(‘lego-stay-time-info’)
  • onHide当中的函数,清除定时,计算一次 ((performance.now() – startTime) / 1000).toFixed(2),与其他数据进行整合,存储本地localStorage.setItem(‘lego-stay-time-info’),获取之后进行lego.send(),清楚本地缓存
    3、init中开启setCloseMark,会默认使用pageId为每个页面设置 setCloseMark,setClostMarkByPageId函数当中获取t值,为空则设置为25,如果存在着[15, 16, 78, 79]的特定值,设置window.ZZAPP[‘setCloseMark’]({ mark: pageId })
    4、legoInit初始化挂载各个函数

legoPagelife

四个周期函数onShow 、onHide、onLoad、onUnload
周期函数执行函数队列onShowFns、onHideFns、onLoadFns、onUnloadFns
三个参数,type, href, time,触发之后会依次执行对应的fn当中的函数

监听页面的history,主要为监听popstate pushState replaceState事件,或者document.hidden触发自定义事件,pageshow,pagehide事件,页面现有的load,unload,hashchange等事件,去触发生命周期hook。比如路由变更,hash变更,document的hidden由false变为true,都去触发页面展现的钩子函数,执行相应队列中的方法。

Page Visibility API

Page Visibility API,监听⻚面的展示/隐藏,⻚面展示的时候则向cookie中追加一个记录。这个 API 本身非常简单,

  • document.visibilityState: 只读属性,表示当前页面的可见状态。它具有以下几个值:

    • "hidden": 页面不可见,即页面在后台或最小化状态。
    • "visible": 页面完全可见。
    • "prerender": 页面正在渲染,但不可见,例如在预渲染阶段。
  • document.hidden: 这是一个只读属性,用于指示当前页面是否可见,表示⻚面是否隐藏。这可能意味着⻚面在后台标签⻚或浏览器中被最小化了。这个值是为了向后兼容才继续被浏览器支持的,应该优先使用 document.visibilityState 检测⻚面可⻅性。如果页面不在用户的视野内,它的值为 true,否则为 false

  • visibilitychange 事件:当页面的可见状态发生变化时,就会触发这个事件。可以通过监听这个事件来执行相应的操作。

legoPerf

lego-perf里边主要用到的有两个 Web API

Performance

Performance 是一个与性能相关的API,可以获取到当前页面中与性能相关的信息

Performance.timing,对象包含延迟相关的性能信息

window.performance.timing 对象提供了许多属性,用于访问有关网页加载和性能的时间信息。以下是这些属性的列表:

  1. navigationStart: 导航开始的时间戳,即浏览器开始获取当前文档的时间。
  2. unloadEventStart: 前一个文档的 unload 事件开始时间。
  3. unloadEventEnd: 前一个文档的 unload 事件结束时间。
  4. redirectStart: 重定向开始的时间戳,即从前一个文档到当前文档的重定向开始时间。
  5. redirectEnd: 重定向结束的时间戳,即从前一个文档到当前文档的重定向结束时间。
  6. fetchStart: 开始获取文档的时间戳,通常是发起请求的时间。
  7. domainLookupStart: 域名解析开始的时间戳,即浏览器开始解析域名的时间。
  8. domainLookupEnd: 域名解析结束的时间戳,即浏览器完成域名解析的时间。
  9. connectStart: 开始建立连接的时间戳,即浏览器开始建立网络连接的时间。
  10. connectEnd: 完成网络连接的时间戳,即浏览器完成网络连接的时间。
  11. secureConnectionStart: 安全连接开始的时间戳,用于HTTPS连接。
  12. requestStart: 开始发送请求的时间戳,即浏览器开始向服务器发送请求的时间。
  13. responseStart: 开始接收响应的时间戳,即浏览器开始接收服务器响应的时间。
  14. responseEnd: 响应结束的时间戳,即浏览器完成接收服务器响应的时间。
  15. domLoading: 开始解析文档对象模型(DOM)的时间戳。
  16. domInteractive: DOM 变为可交互的时间戳,即用户可以与页面进行交互的时间。
  17. domContentLoadedEventStart: DOMContentLoaded 事件开始的时间戳。
  18. domContentLoadedEventEnd: DOMContentLoaded 事件结束的时间戳。
  19. domComplete: DOM 解析完成的时间戳。
  20. loadEventStart: 加载事件开始的时间戳,即文档加载事件开始的时间。
  21. loadEventEnd: 加载事件结束的时间戳,即文档加载事件结束的时间。

通过这些属性,我们了解页面加载和渲染的各个阶段,从而优化页面性能和用户体验。而这些属性值都是时间戳,以毫秒为单位。

Performance.getEntries(FilterOptions),对于给定的filter,此方法返回 PerformanceEntry对象数组,PerformanceEntry对象代表了 performance 时间列表中的单个 metric 数据,metric(MaiTreeKe)可以理解为基本单元,一条记录。手动构建 mark或者measure生成.Performance entries 在资源加载的时候,也会被动生成(例如图片、script、css等资源加载)

这些术语都与前端性能衡量指标和性能度量有关,它们在网页加载和交互时用于评估用户体验。以下是这些术语的解释:

  1. FMP (First Meaningful Paint) :首次有意义绘制,是指页面加载过程中,浏览器第一次渲染了用户实际可见的内容,使用户能够感知到页面的加载进度。FMP 反映了用户首次感受到页面正在加载的时间点。Performance API 中 firstMeaningfulPaint 属性的值,代表页面上首次出现有意义的绘制的时间点。
  2. LCP (Largest Contentful Paint) :最大内容绘制,是指页面加载过程中,最大且有意义的内容绘制完成的时间。通常是页面上最大的图片、视频、文本块等。LCP 反映了用户感知的加载速度,越快越好。Performance API 中 largestContentfulPaint 属性的值,代表页面上最大且有意义的内容绘制完成的时间点。
  3. First Paint Time:首次绘制时间,是指浏览器第一次在屏幕上绘制像素的时间点。它不一定是有意义的内容,只是开始渲染的时间。
  4. FP (First Paint) :首次绘制,指浏览器第一次在屏幕上绘制任何内容,不一定有意义。它通常与页面加载速度有关。
  5. FCP (First Contentful Paint) :首次有内容绘制,是指页面加载过程中,浏览器第一次在屏幕上绘制了任何有意义的内容,可以是文本、图片等。FCP 也反映了用户首次感受到页面正在加载的时间点。Performance API 中 firstContentfulPaint 属性的值,代表页面上首次出现有意义的内容绘制的时间点。
  6. FID (First Input Delay) :首次输入延迟,是指从用户首次与页面交互(如点击链接、按钮等)到浏览器实际响应该交互的时间间隔。FID 反映了页面交互的响应速度,越短越好。它通常通过 Performance API 中的 firstInputDelay 属性来获取。
  7. CLS (Cumulative Layout Shift) :累计布局位移,是指页面加载过程中,不断变化的内容导致元素位置发生变化的总和。CLS 反映了页面在加载过程中的稳定性,避免元素突然移动。

Mutation Observer 接口提供了监视对DOM树所做更改的能力,通过new实例化后,调用observe()开始观察,disconnect()停止观察

performance.getEntries()

通过 performance.getEntries() 获取各个资源请求的 PerformanceEntry 对象,统计耗时
performanceEntries 是指浏览器性能API中的 PerformanceEntry 对象数组。这个数组包含了在页面生命周期中收集的不同类型的性能数据,比如资源加载时间、导航信息、用户交互延迟等等。

这些数据是通过 Performance API 提供的接口,如 performance.getEntries()performance.getEntriesByType(type) 来获取的。在这个返回的数组中,每个 PerformanceEntry 对象都代表了特定的性能指标或事件。不同类型的性能指标(如资源加载、用户交互等)会有不同的属性。

常见的 PerformanceEntry 对象属性包括:

  • name: 标识资源或事件的名称。
  • entryType: 表示性能条目的类型,如 “resource”、”navigation” 等。
  • startTime: 开始时间,表示性能事件的开始时间。
  • duration: 持续时间,表示性能事件的时长。
  • initiatorType: 发起者类型,用于资源加载事件,如 “script”、”img” 等。
  • 其他类型特定的属性,如 decodedBodySizetransferSize 等,用于描述资源加载事件。

通过遍历 performanceEntries 数组,可以获取页面中不同类型的性能数据,进而进行分析、优化和监控。这对于了解页面性能并针对性地进行改进非常有帮助。

统计页面性能–> lego-perf-perfomance perf.ts
资源性能数据–> lego-perf-perfomance resource.ts
预渲染页参数特殊处理
setPrerenderData(params, store)白屏参数设置默认值domReady ssr 的首屏时间,
如果没有计算首屏时间,则给默认值 100ms,完全加载时间

  const ptiming = performance.timing
// 白屏时间
result.blankTime = fix(ptiming.responseStart - ptiming.navigationStart)
// 重定向开始
result.rdStartTime = fix(ptiming.redirectStart - ptiming.navigationStart)
// 重定向结束
result.rdEndTime = fix(ptiming.redirectEnd - ptiming.navigationStart)
// 浏览器发起请求的时间
result.fStartTime = fix(ptiming.fetchStart - ptiming.navigationStart)
// connect开始
result.cStartTime = fix(ptiming.connectStart - ptiming.navigationStart)
// connect结束
result.cEndTime = fix(ptiming.connectEnd - ptiming.navigationStart)
// dns开始时间
result.dnsStartTime = fix(ptiming.domainLookupStart - ptiming.navigationStart)
// dns结束时间
result.dnsEndTime = fix(ptiming.domainLookupEnd - ptiming.navigationStart)
// dns耗时
result.dnsTime = fix(ptiming.domainLookupEnd - ptiming.domainLookupStart)
// 浏览器开始接受数据的时间,首包时间,即ttf时间, 浏览器收到从服务端(或缓存,本地资源)响应回的第一个字节的数据的时刻
result.fpTime = fix(ptiming.responseStart - ptiming.navigationStart)
// http请求耗时
result.fpEndTime = fix(ptiming.responseEnd - ptiming.navigationStart)
// 开始解析收到的 HTML 页面的第一个字节。
result.domStartTime = fix(ptiming.domLoading - ptiming.navigationStart)
// 完成所有 script 加载和解析之后, 当 DOMContentLoaded 事件触发之前,浏览器完成所有script(包括设置了defer的script)的下载和解析之后的时刻
// result.domContentLoadedEventStart = fix(ptiming.domContentLoadedEventStart - ptiming.navigationStart)
// document 文档解析完成,不包括异步js,即 readyState 变为'interactive',readystartechange触发,指文档解析完成的时刻,包括在“传统模式”下被阻塞的通过script标签加载的内容(除了使用defer或者async属性异步加载的情况)
// 文档已被解析,"正在加载" 状态结束, DOM 元素可以被访问, 但是诸如图像,样式表和框架之类的子资源仍在加载
result.domInteractiveTime = fix(ptiming.domInteractive - ptiming.navigationStart)
// dom 完全构建包括样式
result.domLoadedTime = fix(ptiming.domContentLoadedEventStart - ptiming.navigationStart)
// domReady 时间 ,SSR 项目首屏时间
result.readyTime = fix(ptiming.domContentLoadedEventEnd - ptiming.navigationStart)
if (!result.atfStartTime) result.atfStartTime = result.readyTime
// 可操作时间,包括使用图片,document.readyState 属性设置为 "complete" 与 onload 近似
result.activeTime = fix(ptiming.domComplete - ptiming.navigationStart)
// _ZZ_WEBVIEW_CREATE_TIME_  该变量为客户端 webview 注入
if (window?._ZZ_WEBVIEW_CREATE_TIME_) {
result.webviewInitTime = fix(ptiming.navigationStart - +window?._ZZ_WEBVIEW_CREATE_TIME_)
result.blankTime = fix(result.blankTime + result.webviewInitTime)
}
result.loadTime = Math.max(
fix(ptiming.loadEventEnd - ptiming.navigationStart),
fmp ? fmp + 100 : 0
)
if (result.loadTime !== 0) {
result.atfTime = fmp || result.loadTime
} else {
result.atfTime = result.activeTime
}
return result
}

fmp.ts

FMP(first meaning paint)首次有效绘制时间,标准组织推出了 performance timing api 提供了在加载和使用当前页面期间发生的各种事件的性能计时信息。

我们要取FMP,其实就是一个页面加载的内容是已经足够所需的耗时,如何确定页面已经处于useful状态?用户可以看见的展示信息的元素都已稳定呈现,通过 Mutation Observer 和 calculateScore 函数进行实现

初始化 Mutation Observer

initObserver,初始化 Mutation Observer,监听document文档下的所有dom结构变化,存在document.body,则触发calculateScore,并将该节点的Date.now() – performance.timing.fetchStart和score作为对象的属性,保存在SCORE_ITEMS数组中。

var observerOptions = {
 childList: true,  // 观察目标子节点的变化,是否有添加或者删除
 attributes: true, // 观察属性变动
 subtree: true     // 观察后代节点,默认为 false
}

calculateScore具体执行

// DOM结构算分
const calculateScore = (el: any, tiers?: number, parentScore?: boolean) => {
try {
let score = 0
const tagName = el.tagName
//不是script style meta head标签
if ('SCRIPT' !== tagName && 'STYLE' !== tagName && 'META' !== tagName && 'HEAD' !== tagName) {
const childrenLen = el.children ? el.children.length : 0 
//计算当前子节点数量
if (childrenLen > 0) { //遍历子节点  有一个节点tiers就加一
for (let child = el.children, len = childrenLen - 1; len >= 0; len--)        {
score += calculateScore(child[len], tiers + 1, score > 0)
//递归计算总分
}
}
if (score <= 0 && !parentScore) {
if (!(el.getBoundingClientRect && el.getBoundingClientRect().top < WH())) return 0
}
score += 1 + 0.5 * tiers //权重 父节点为1,子节点为0.5 当前节点的一个分数 
}
return score
} catch (error) {}
}

先递归再进行是否在视口内的判断,这样每一个子元素都可以判断到,防止父元素不在视口内但子元素通过定位到视口内的情况

calFinallyScore结算
dom结构变化就重新算分,当所有资源加载完毕,即 readyState === complete直接触发,或者监听window.load 事件或者window.beforeunload事件来进行最终算分

  1. 判断DOM的变化次数是否大于4次,即SCORE_ITEMS 元素是否超过四个,如果是的话,就找出最后一次变化时间,判断当前的时间与最后变化的元素的时间节点时间差是否大于固定的2倍时间间隔,即当前时间 – 最后一个 SCORE_ITEMS 元素渲染时间差 > 2 * 500(即两次 calFinallyScore 执行时间)如果是的话,就代表,元素不再变化的时间点,Fmp 已经完成。
  2. 判断如果 DOM 变化次数超过10次,即SCORE_ITEMS 元素超过10个,并且当前页面window.performance.timing.loadEventEnd !== 0,loadEventEnd属性具体表示当前文档加载事件完成的时间戳。加载事件是指所有网页资源(如图像、脚本、样式表)都已加载完成,文档被视为完全加载的时间点),即表示load 结束。SCORE_ITEMS 最后一个元素的分数为倒数第9个元素分数,说明load 结束并且 dom 不再变化,而最后一次 和 第一次的分数相同,表示该DOM结构没有发生变化。

上述条件表示fmp已经结束,之后取消 MutationObserver,this.observer.disconnect()

根据SCORE_ITEMS数组当中的数据,过滤掉 SCORE_ITEMS[n] < SCORE_ITEMS[n-1] 的元素,与最大差值无关。

计算出分数变化最大时的时间点,差值最大即为fmp时间节点,大于10s会自动被过滤。

总结

通过监听dom节点变化,记录每一次变化的时间节点,触发计算机制,得出当前dom计算出来的分数,通过监听页面 load,beforeunload等事件,来触发最终计算fmp的时机,即calFinallScore函数

通过判断dom的变化次数,判断时间间隔,以及对比分数的变化情况来判断是否已经可以来计算FMP。

  • 如果不通过,就间隔500ms再来判断。
  • 如果通过,就开始做最终的计算。
  • 遍历所有的时间节点,寻找分数差值最大的时间节点,即为fmp的时间节点
  • 然后再获取页面中所有图片的加载时长,计算出最后一张图片的加载时间,即(performance.getEntriesByName(element)[0] as PerformanceResourceTiming)
    ?.responseEnd
  • 最后通过 setPerformance 方法设置到 perf 上面去。
//用户在加载资源2000毫秒后进行的touch操作将进行最终算分
function listenTouchstart() {
   if (Date.now() - performance.timing.fetchStart > 2000) {
       //if (Date.now() > 2000) {
       that.calFinallScore();
       that.mark = 'touch';
       window.removeEventListener('touchstart', listenTouchstart, true);
   }
}

legoExposure legoAdExposure

只讲核心部分。

讲清楚什么时候触发为重中之重。

大多借助IntersectionObserver这个api

// IntersectionObserver属性的处理
const ISObserver = () => {
observer = new IntersectionObserver(
(entries) => {
let maxMetric = ''
entries.forEach((entry) => {
if (entry.intersectionRatio && entry.intersectionRatio > 0) {
maxMetric = entry.target.getAttribute('data-ad-ticket')
// send lego
if (maxMetric) sendLegoLog(maxMetric)
}
})
},
{
threshold: 0.5
}
)
targetArr.forEach(function(item) {
observer.observe(item)
})
}
// inviewport判断取可视区的最大值
const ExposureCollector = () => {
let maxMetric = ''
for (let i = 0; i < targetArr.length; i++) {
if (inviewport(targetArr[i], height, width)) {
maxMetric = targetArr[i].getAttribute('data-ad-ticket')
// send lego
if (maxMetric) sendLegoLog(maxMetric)
}
}
}

inviewport即借助getBoundingClientRect对象实现的

防抖滚动触发上报埋点

// 监听页面滚动上报  使用防抖
const exposureDefault = (() => {
let initFlag = false
return () => {
// 判断上下滚动
let startScrollTop = 0
if (initFlag) return
initFlag = true
//监听滚动事件
window.addEventListener(
'scroll',
(e) => {
e.stopPropagation()
const nextScrollTop =
document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop
// 下滚动才发埋点
if (nextScrollTop - startScrollTop > 0) {
try {
act({})
} catch (err) {}
}
startScrollTop = nextScrollTop
},
false
)
}
})()

legoAreaExposure

接触即触发,与IntersectionObserverEntry和IntersectionObserver对象相关。

// 元素出现在可视区域时的操作逻辑
const observerLogic = (entries: IntersectionObserverEntry[]) => {
// console.log('entries', entries)
const ctx: {
metric?: string
isdot?: string
backupParam?: string
uid?: string
backupParamObj?: Backup
} = {}
entries.forEach((entry) => {
// 元素至少50%可见   解构entry,赋值给ctx对象   交叉比例过0
if (entry.intersectionRatio && entry.intersectionRatio > 0) {
ctx.metric =
entry.target.getAttribute(AREA_ATTR_NAME) || entry.target.getAttribute(SECTION_ID_NAME) // 可见元素的指标
ctx.isdot = entry.target.getAttribute(DATA_ISDOT) // 是否是dot
ctx.backupParam =
entry.target.getAttribute(BACK_UP_NAME) || entry.target.getAttribute(ZZ_BACKUP) || '{}' // 备份参数
ctx.uid = ctx.metric + ctx.backupParam // 唯一标识
try {
ctx.backupParamObj = JSON.parse(ctx.backupParam) // 将备份参数解析为JS对象
} catch (e) {
Logger.error('[Lego - legoAreaExposure] lego-backup参数格式错误,无法解析成JS对象') // 备份参数格式错误
}
if (ctx.metric) {
legoLog(ctx) // 发送LEGO指标
}
}
})
}

legoGoodsExposure

监听元素相交即push,上报,维护所有 MutationObserver 实例, 防止重复监听出现

  ioPromise.then(() => {
const newISObserver = new IntersectionObserver(
(entries) => {
// 元素出现在可视区的操作逻辑
let goodInfo = {}
entries.forEach((entry) => {
// 可见元素的可见比例大于0
if (entry.intersectionRatio && entry.intersectionRatio > 0) {
goodInfo = entry.target.getAttribute(GOODS_EXPOSURE_NAME)
if (!goodInfo) return
try {
goodInfo = JSON.parse(goodInfo as string)
} catch (e) {
Logger.error(
`[Lego - legoGoodsExposure] 商品数据格式错误,无法转换为JS对象,请检查。错误数据:'${goodInfo}`
)
goodInfo = {}
}
const old = cachedGoodInfo.get(targetContainer)
if (!old) {
cachedGoodInfo.set(params.targetContainer, [goodInfo])
} else {
old.push(goodInfo)
}
sendGoodsExposure(params, callback)()
}
})
},
{
threshold: 0.5 // 元素50%显示时,即认为出现在了可视区
}
)
targetGoods.forEach((item) => newISObserver.observe(item))
ISObserverMap.set(targetContainer, newISObserver)
})

原文链接:https://juejin.cn/post/7265891728163667983 作者:原野风殇

(0)
上一篇 2023年8月12日 上午10:51
下一篇 2023年8月12日 上午11:02

相关推荐

发表回复

登录后才能评论