前端监控 SDK 的一些技能要点道理阐发

发布日期:2022-08-07 05:47    点击次数:86

 

一个完备的前端监控平台蕴含三个部份:数据收集与上报、数据摒挡和存储、数据展现。

本文要讲的就是个中的第一个环节——数据收集与上报。下图是本文要奉告内容的纲目,巨匠可以或许先大致相识一下:

仅看实践知识是相比难以理解的,为此我联结本文要讲的技能要点写了一个俭朴的监控 SDK[1],可以或许用它来写一些俭朴的 DEMO,协助加深理解。再联结本文一起浏览,结果更好。

性能数据收集

chrome 开发团队提出了一系列用于检测网页性能的指标:

 FP(first-paint),从页面加载起头到第一个像素绘制到屏幕上的时光  FCP(first-contentful-paint),从页面加载起头到页面内容的任何部份在屏幕上实现衬着的时光  LCP(largest-contentful-paint),从页面加载起头到最大文本块或图像元素在屏幕上实现衬着的时光  CLS(layout-shift),从页面加载起头和其生命周期形态[2]变为潜匿时期发生的所故意外计划偏移的积攒分数

这四共性能指标都须要经由过程 PerformanceObserver[3] 来取得(也可以经由过程 performance.getEntriesByName() 取得,但它不是在事宜触发时看护的)。PerformanceObserver 是一共性能监测工具,用于监测性能度量事宜。

FP

FP(first-paint),从页面加载起头到第一个像素绘制到屏幕上的时光。实在把 FP 理解成白屏时光也是没成就的。

测量代码以下: 

const entryHandler = (list) => {           for (const entry of list.getEntries()) {          if (entry.name === 'first-paint') {              observer.disconnect()          }         console.log(entry)      }  } const observer = new PerformanceObserver(entryHandler)  // buffered 属性默示是否窥察缓存数据,也就是说窥察代码增加机遇比工作触发机遇晚也无妨。  observer.observe({ type: 'paint', buffered: true }) 

经由过程以上代码可以或许失去 FP 的内容: 

{      duration: 0,      entryType: "paint",      name: "first-paint",      startTime: 359, // fp 时光  } 

个中 startTime 就是我们要的绘制时光。

FCP

FCP(first-contentful-paint),从页面加载起头到页面内容的任何部份在屏幕上实现衬着的时光。关于该指标,"内容"指的是文本、图像(蕴含背景图像)、<svg>元素或非赤色的<canvas>元素。

为了供应杰出的用户休会,FCP 的分数该当掌握在 1.8 秒以内。

测量代码: 

const entryHandler = (list) => {           for (const entry of list.getEntries()) {          if (entry.name === 'first-contentful-paint') {              observer.disconnect()          }                  console.log(entry)      }  }  const observer = new PerformanceObserver(entryHandler)  observer.observe({ type: 'paint', buffered: true }) 

经由过程以上代码可以或许失去 FCP 的内容: 

{      duration: 0,      entryType: "paint",      name: "first-contentful-paint",      startTime: 459, // fcp 时光  } 

个中 startTime 就是我们要的绘制时光。

LCP

LCP(largest-contentful-paint),从页面加载起头到最大文本块或图像元素在屏幕上实现衬着的时光。LCP 指标会痛处页面初度起头加载[4]的时光点来报告可视地区内可见的最大图像或文本块[5]实现衬着的相对时光。

一个杰出的 LCP 分数该当掌握在 2.5 秒以内。

测量代码: 

const entryHandler = (list) => {      if (observer) {          observer.disconnect()      }      for (const entry of list.getEntries()) {          console.log(entry)      }  }  const observer = new PerformanceObserver(entryHandler)  observer.observe({ type: 'largest-contentful-paint', buffered: true })

经由过程以上代码可以或许失去 LCP 的内容: 

{      duration: 0,      element: p,      entryType: "largest-contentful-paint",      id: "",      loadTime: 0,      name: "",      renderTime: 1021.299,      size: 37932,      startTime: 1021.299,      url: "",  } 

个中 startTime 就是我们要的绘制时光。element 是指 LCP 绘制的 DOM 元素。

FCP 和 LCP 的不同是:FCP 只需肆意内容绘制实现就触发,LCP 是最大内容衬实在现时触发。

LCP 审核的元素范例为:

 <img>元素  内嵌在<svg>元素内的<image>元素  <video>元素(运用封面图像)  经由过程[url()](https://link.juejin.cn?target=https://developer.mozilla.org/docs/Web/CSS/url( "url()"))函数(而非运用CSS 渐变[6])加载的带有背景图像的元素  包孕文本节点或别的行内级文本元素子元素的块级元素[7]。 CLS

CLS(layout-shift),从页面加载起头和其生命周期形态[8]变为潜匿时期发生的所故意外计划偏移的积攒分数。

计划偏移分数的计算要领以下: 

计划偏移分数 = 影响分数 * 距离分数 

影响分数[9]测量不奔忙动元素对两帧之间的可视地区孕育发生的影响。

距离分数指的是任何不奔忙动元素在一帧中位移的最大距离(水平或垂直)除以可视地区的最大尺寸维度(宽度或高度,以较大者为准)。

CLS 就是把全体计划偏移分数加起来的总和。

当一个 DOM 在两个衬着帧之间孕育发生了位移,就会触发 CLS(如图所示)。

上图中的矩形从左上角移动到了右侧,这就算是一次计划偏移。同时,在 CLS 中,有一个叫会话窗口的术语:一个或多个倏地间断发生的单次计划偏移,每次偏移相隔的时光少于 1 秒,且全副窗口的最大继续时长为 5 秒。

譬如上图中的第二个会话窗口,它内里有四次计划偏移,每一次偏移之间的距离必须少于 1 秒,并且第一个偏移和最后一个偏移之间的时光不克不迭逾越 5 秒,这样材干算是一次会话窗口。假定不吻合这个条件,就算是一个新的会话窗口。可以或许有人会问,为何要这样规定?实在这是 chrome 团队痛处大量的试验和研究得出的阐发终局 Evolving the CLS metric[10]。

CLS 一共有三种计算要领:

 累加  取全体味话窗口的匀称数  取全体味话窗口中的最大值

累加

也就是把从页面加载起头的全体计划偏移分数加在一起。然则这类计算要领对生命周期长的页面不敌对,页面存留时光越长,CLS 分数越高。

取全体味话窗口的匀称数

这类计算要领不是按单个计划偏移为单位,而因此会话窗口为单位。将全体味话窗口的值相加再取匀称值。然则这类计算要领也出弱点。

从上图可以或许看进去,第一个会话窗口孕育发生了相比大的 CLS 分数,第二个会话窗口孕育发生了相比小的 CLS 分数。假定取它们的匀称值来当成 CLS 分数,则基本看不进去页面的运行状况。原来页面是晚期偏移多,后期偏移少,而今的匀称值没法回响反映出这类环境。

取全体味话窗口中的最大值

这类要领是而今最优的计算要领,每次只取全体味话窗口的最大值,用来回响反映页面计划偏移的最差环境。概况请看 Evolving the CLS metric。

上面是第三种计算要领的测量代码: 

let sessionValue = 0  let sessionEntries = []  const cls = {      subType: 'layout-shift',      name: 'layout-shift',      type: 'performance',      pageURL: getPageURL(),      value: 0,  }  const entryHandler = (list) => {      for (const entry of list.getEntries()) {          // Only count layout shifts without recent user input.          if (!entry.hadRecentInput) {              const firstSessionEntry = sessionEntries[0]              const lastSessionEntry = sessionEntries[sessionEntries.length - 1]               // If the entry occurred less than 1 second after the previous entry and              // less than 5 seconds after the first entry in the session, include the              // entry in the current session. Otherwise, start a new session.              if (                  sessionValue                  && entry.startTime - lastSessionEntry.startTime < 1000                  && entry.startTime - firstSessionEntry.startTime < 5000              ) {                  sessionValue += entry.value                  sessionEntries.push(formatCLSEntry(entry))              } else {                  sessionValue = entry.value                  sessionEntries = [formatCLSEntry(entry)]              }              // If the current session value is larger than the current CLS value,              // update CLS and the entries contributing to it.              if (sessionValue > cls.value) {                  cls.value = sessionValue                  cls.entries = sessionEntries                  cls.startTime = performance.now()                  lazyReportCache(deepCopy(cls))              }          }      }  }  const observer = new PerformanceObserver(entryHandler)  observer.observe({ type: 'layout-shift', buffered: true }) 

在看完上面的文字形貌后,再看代码就好懂患有。一次计划偏移的测量内容以下: 

{     duration: 0,    entryType: "layout-shift",公司资讯NEWS     hadRecentInput: false,     lastInputTime: 0,     name: "",     sources: (2) [LayoutShiftAttribution, LayoutShiftAttribution],     startTime: 1176.199999999255,     value: 0.000005752046026677329,  } 
代码中的 value 字段就是计划偏移分数。 DOMContentLoaded、load 事宜

当纯 HTML 被齐全加载以及剖析时,DOMContentLoaded 事宜会被触发,不消等待 css、img、iframe 加载完。

当全副页面及全体寄托资源如款式表和图片都已实现加载时,将触发 load 事宜。

诚然这两共性能指标相比旧了,然则它们仍然能回响反映页面的一些环境。关于它们举行监听仍然是须要的。 

import { lazyReportCache } from '../utils/report'  ['load', 'DOMContentLoaded'].forEach(type => onEvent(type))  function onEvent(type) {         function callback() {            lazyReportCache({                   type: 'performance',                  subType: type.toLocaleLowerCase(),                 startTime: performance.now(),               })                     window.removeEventListener(type, callback, true)         }            window.addEventListener(type, callback, true)  } 
首屏衬着时光

大大都环境下,首屏衬着时光可以或许经由过程 load 事宜取得。除了一些不凡环境,譬如异步加载的图片和 DOM。 

<script>      setTimeout(() => {             document.body.innerHTML = `               <div>                             <!-- 省略一堆代码... -->                   </div>            `      }, 3000)  </script

像这类环境就没法经由过程 load 事宜取得首屏衬着时光了。这时候我们须要经由过程 MutationObserver[11] 来取得首屏衬着时光。MutationObserver 在监听的 DOM 元素属性发生变换时会触发事宜。

首屏衬着时光计算进程:

 行使 MutationObserver 监听 document 工具,每当 DOM 元素属性发生厘革时,触发事宜。  鉴定该 DOM 元素是否在首屏内,假定在,则在 requestAnimationFrame() 回调函数中调用 performance.now() 取得今后时光,作为它的绘制时光。  将最后一个 DOM 元素的绘制时光和首屏中全体加载的图半晌光作对比,将最大值作为首屏衬着时光。

监听 DOM 

const next = window.requestAnimationFrame ? requestAnimationFrame : setTimeout  const ignoreDOMList = ['STYLE', 'SCRIPT', 'LINK']    observer = new MutationObserver(mutationList => {       const entry = {              children: [],        }            for (const mutation of mutationList) {             if (mutation.addedNodes.length && isInScreen(mutation.target)) {                       // ...              }         }        if (entry.children.length) {              entries.push(entry)              next(() => {                       entry.startTime = performance.now()             })       }  })  observer.observe(document, {        childList: true,        subtree: true,  }) 

上面的代码就是监听 DOM 变换的代码,同时须要过滤掉 style、script、link 等标签。

鉴定是否在首屏

一个页面的内容可以或许极度多,但用户至多只能瞥见一屏幕的内容。所以在统计首屏衬着时光的时光,须要限制领域,把衬着内容限制在今后屏幕内。 

const viewportWidth = window.innerWidth  const viewportHeight = window.innerHeight  // dom 工具是否在屏幕内  function isInScreen(dom) {       const rectInfo = dom.getBoundingClientRect()        if (rectInfo.left < viewportWidth && rectInfo.top < viewportHeight) {                return true        }         return false  } 

运用 requestAnimationFrame() 取得 DOM 绘制时光

当 DOM 厘革触发 MutationObserver 事宜时,只是代表 DOM 内容可以或许被读取到,实在不代表该 DOM 被绘制到了屏幕上。

从上图可以或许看出,当触发 MutationObserver 事宜时,可以或许读取到 document.body 上已经有内容了,但实践上右边的屏幕并无绘制任何内容。所以要调用 requestAnimationFrame() 在浏览器绘制告成后再取得今后时光作为 DOM 绘制时光。

和首屏内的全体图片加载时光作对比 

function getRenderTime() {         let startTime = 0        entries.forEach(entry => {            if (entry.startTime > startTime) {               startTime = entry.startTime            }       })         // 须要和今后页面全体加载图片的时光做对比,取最大值         // 图片要求时光要小于 startTime,照顾终止时光要大于 startTime       performance.getEntriesByType('resource').forEach(item => {               if (                 item.initiatorType === 'img'                  && item.fetchStart < startTime                   && item.responseEnd > startTime             ) {                       startTime = item.responseEnd             }        })             return startTime  } 

优化

而今的代码还没优化完,次要有两点留心事故:

 何时上报衬着时光?  假定兼容异步增加 DOM 的环境?

第一点,必须要在 DOM 再也不变换后再上报衬着时光,普通 load 事宜触发后,DOM 就再也不变换了。所以我们可以或许在这个时光点举行上报。

第二点,可以或许在 LCP 事宜触发后再举行上报。不论是同步照旧异步加载的 DOM,它都须要举行绘制,所以可以或许监听 LCP 事宜,在该事宜触发后才准许举行上报。

将以上两点计划联结在一起,就有了下列代码: 

let isOnLoaded = false  executeAfterLoad(() => {        isOnLoaded = true  })  let timer  let observer  function checkDOMChange() {        clearTimeout(timer)       timer = setTimeout(() => {              // 等 load、lcp 事宜触发后并且 DOM 树再也不变换时,计算首屏衬着时光               if (isOnLoaded && isLCPDone()) {                  observer && observer.disconnect()                   lazyReportCache({                            type: 'performance',                      subType: 'first-screen-paint',                     startTime: getRenderTime(),                        pageURL: getPageURL(),                    })                              entries = null               } else {                    checkDOMChange()              }        }, 500)  } 

checkDOMChange() 代码每次在触发 MutationObserver 事宜时举行调用,须要用防抖函数举行处理惩罚。

接口要求耗时

接口要求耗时须要对 XMLHttpRequest 和 fetch 举行监听。

监听 XMLHttpRequest 

originalProto.open = function newOpen(...args) {        this.url = args[1]        this.method = args[0]         originalOpen.apply(this, args)  }  originalProto.send = function newSend(...args) {        this.startTime = Date.now()            const onLoadend = () => {            this.endTime = Date.now()           thisthis.duration = this.endTime - this.startTime                    const { status, duration, startTime, endTime, url, method } = this                const reportData = {                  status,                     duration,                     startTime,                     endTime,                       url,                     method: (method 


相关资讯