手写前端监控+页面性能监控(附性能监控基础知识 + 代码 + 基础版本改进建议)

前端监控

前端监控似乎现在是一个炙手可热的话题,想要成为一个不只会写业务代码的进阶前端,少不了对页面性能,功能的监控,由此出现前端监控的概念,本文介绍了主要介绍了以下几个内容

  • 前端监控该从哪些方面进行
  • 模块监控内部如何实现
  • 监控中涉及的页面性能监控的基础知识讲解
  • 关于如何使用前端监控以及可以进行优化的点
  • 还有什么要做的和注意的
一、前端监控分类

针对前端监控我们可以大致将前端监控模块分为如下几个部分:

  1. 用户行为监控:用于收集用户在网站或应用中的操作信息,例如点击、滚动、输入等。
  2. 性能监控:用于收集页面加载和渲染的相关性能指标,例如页面加载时间、白屏时间等。
  3. 异常监控:用于捕获并上报前端代码运行过程中出现的异常和错误。
  4. 资源监控:用于监控页面中各种资源(例如图片、脚本、样式表等)的加载情况,以及它们对页面性能的影响。
  5. 其他监控:例如:数据上报模块(uploader) : 处理上报数据,控制台打印模块(logger) : 控制台打印上报记录,插件模块(plugin) : 可外接其他监控模块。 分类展示为下图

image.png

二、代码实现前端监控(简单版本)
1、用户行为监控模块:

用户行为监控模块:针对用户点击、滚动、输入、使用document.addEventListener进行监控,根据传递参数的不同,监控不同的事件。

// 定义一个发送数据的函数
function sendData(data) {
  // 在这里,您可以使用AJAX、Fetch或其他方法将数据发送到服务器
  // 例如:
  // fetch('/api/track', {
  //   method: 'POST',
  //   body: JSON.stringify(data),
  //   headers: {
  //     'Content-Type': 'application/json'
  //   }
  // });
}

// 监听点击事件
document.addEventListener('click', function(event) {
  // 获取点击的元素
  var target = event.target;
  console.log('我点击了~~',event);

  // 获取元素的相关信息,例如ID、类名等
  var id = target.id;
  var className = target.className;

  // 构造要发送的数据
  var data = {
    type: 'click',
    id: id,
    className: className,
    // 其他您想要收集的信息
  };

  // 发送数据
  sendData(data);
});

// 监听滚动事件
document.addEventListener('scroll', function(event) {
  // 获取滚动位置
  var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

  // 构造要发送的数据
  var data = {
    type: 'scroll',
    scrollTop: scrollTop,
    // 其他您想要收集的信息
  };

  // 发送数据
  sendData(data);
});

// 监听输入事件
document.addEventListener('input', function(event) {
  // 获取输入的元素和值
  var target = event.target;
  var value = target.value;

  // 构造要发送的数据
  var data = {
    type: 'input',
    value: value,
    // 其他您想要收集的信息
  };

  // 发送数据
  sendData(data);
});
复制代码
2、性能监控模块:
(一)基础知识

讲解页面性能监控之前需要先普及一点页面性能监控指标的基础知识,以便于大家更好的理解这里,首先,前端页面性能指标通常包括以下几种:

  • 页面加载时间:指从用户发起请求到页面完全呈现在屏幕上所经过的时间。这个指标反映了用户等待页面加载的时间,对用户体验有很大影响。

  • 首字节时间:指从用户发起请求到浏览器接收到服务器响应的第一个字节所经过的时间。这个指标反映了服务器响应速度和网络延迟。

  • 首屏时间:指从用户发起请求到浏览器首次渲染页面内容所经过的时间。这个指标反映了用户等待看到页面内容的时间,对用户体验有很大影响。

  • 白屏时间:指从用户发起请求到浏览器开始渲染页面内容所经过的时间。这个指标反映了用户等待看到页面内容的时间,对用户体验有很大影响。

  • FCP(First Contentful Paint) :指浏览器首次渲染来自DOM的任何文本、图像、非白色画布或SVG的时间。这个指标反映了用户等待看到页面内容的时间,对用户体验有很大影响。

  • LCP(Largest Contentful Paint) :指浏览器渲染页面中最大可见元素的时间。这个指标反映了用户等待看到页面主要内容的时间,对用户体验有很大影响。

    扫描二维码关注公众号,回复: 14779804 查看本文章
  • 阻塞总时长(TBT):表示页面加载过程中所有长任务阻塞主线程的总时间。它也可以通过监听页面上的长任务来计算。当一个长任务的执行时间超过 50 毫秒时,我们认为它阻塞了主线程。

  • 可交互时间(TTI):指从用户发起请求到页面可以正常响应用户操作所经过的时间。这个指标反映了用户等待页面可交互的时间,对用户体验有很大影响。

TTI(Time to Interactive)和 TBT(Total Blocking Time)是两个与页面交互性相关的性能指标。然而,它们不能直接通过 performance API 来获取,需要使用其他方法来计算。会在代码中体现。

除了上述指标,还有许多其他的性能指标,例如DNS查询时间、TCP连接时间、DOM解析时间、资源加载时间等。我们可以根据需要选择合适的性能指标进行监控和优化。

针对上面这些性能指标是可以使用一些工具去检测的,比如

  • Chrome 自带的开发者工具:Performance
  • Lighthouse(灯塔) 开源工具
  • 原生 Performance API
  • 各种官方库、插件
(二)代码部分

接下来进入页面性能检测代码部分,内部同样写了一个sendData函数,使用window.addEventListener('load',function)完成监听,其中performance是一个可以直接使用的对象,由chrome浏览器提供,里面涵盖很多属性,大家可以自行去根据需要增加或者删除。

// 定义一个发送数据的函数
function sendData(data) {
  console.log('我才不要每次都触发呢',data);
  setTimeout(()=> {
    // 在这里,您可以使用AJAX、Fetch或其他方法将数据发送到服务器
    // 例如:
    // fetch('/api/track', {
    //   method: 'POST',
    //   body: JSON.stringify(data),
    //   headers: {
    //     'Content-Type': 'application/json'
    //   }
    // });
  }, 10000)
}

// 监听页面加载事件
window.addEventListener('load', function() {
  // 获取性能数据
  const [performanceData] = performance.getEntriesByType("navigation");
  // 即将废弃 推荐上面的PerformanceNavigationTiming写法
  // var performanceData = window.performance.timing;
  
  // 计算页面加载时间
  var pageLoadTime = performanceData.domContentLoadedEventEnd - performanceData.navigationStart;
  // 计算请求响应时间
  const requestResponseTime = performanceData.responseEnd - performanceData.requestStart;

  // 计算DNS查询时间
  var dnsLookupTime = performanceData.domainLookupEnd - performanceData.domainLookupStart;

  // 计算TCP连接时间
  var tcpConnectTime = performanceData.connectEnd - performanceData.connectStart;

  // 计算白屏时间
  var whiteScreenTime = performanceData.responseStart - performanceData.navigationStart;
  
  // 获取 FCP 时间
  let fcpTime = 0;
  const [fcpEntry] = performance.getEntriesByName("first-contentful-paint");
  if (fcpEntry) {
    fcpTime = fcpEntry.startTime;
  }

  // 获取 LCP 时间
  let lcpTime = 0;
  const lcpEntries = performance.getEntriesByType("largest-contentful-paint");
  if (lcpEntries.length > 0) {
    lcpTime = lcpEntries[lcpEntries.length - 1].renderTime || lcpEntries[lcpEntries.length - 1].loadTime;
  }
  
  // Paint Timing
  const paintMetrics = performance.getEntriesByType('paint');
  paintMetrics.forEach((metric) => {
    console.log(metric.name + ': ' + metric.startTime + 'ms');
  });
 
    // 监听长任务
    let tti = 0;
    let tbt = 0;
    const observer = new PerformanceObserver((entryList) => {
      for (const entry of entryList.getEntries()) {
        // 计算 TBT
        if (entry.duration > 50) {
          tbt += entry.duration - 50;
        }
      }

      // 计算 TTI
      if (tti === 0 && tbt < 50) {
        tti = performance.now();
      }
    });
    observer.observe({ entryTypes: ["longtask"] });
    
  // 构造要发送的性能数据
  var perfData = {
    type: 'performance',
    pageLoadTime: pageLoadTime,
    dnsLookupTime: dnsLookupTime,
    tcpConnectTime: tcpConnectTime,
    whiteScreenTime: whiteScreenTime,
    requestResponseTime: requestResponseTime,
    tbt:tbt,
    tti:tti
    // 其他您想要收集的信息
  };
  
  

  // 发送性能数据
  sendData(perfData);
  });
});

复制代码

注意这里performance.getEntriesByType("navigation")等同于window.performance.timing,并且这里获取到的performanceData 主要用于测量浏览器加载 HTML 文档所需的时间,而如果是performance.getEntriesByType("resource")值的是获取资源(css,脚本,图片等)的时间。 performanceData参数信息如下图:

image.png

(三)错误监控模块
function sendData(data) {
  console.log('我才不要每次都触发呢',data);
  setTimeout(()=> {
    // 在这里,您可以使用AJAX、Fetch或其他方法将数据发送到服务器
    // 例如:
    // fetch('/api/track', {
    //   method: 'POST',
    //   body: JSON.stringify(data),
    //   headers: {
    //     'Content-Type': 'application/json'
    //   }
    // });
  }, 10000)
}
// 监听错误事件
window.addEventListener('error', function(event) {
  // 获取错误信息
  var message = event.message;
  var filename = event.filename;
  var lineno = event.lineno;
  var colno = event.colno;

  // 构造要发送的数据
  var data = {
    type: 'error',
    message: message,
    filename: filename,
    lineno: lineno,
    colno: colno,
    // 其他您想要收集的信息
  };

  // 发送数据
  sendData(data);
});
复制代码

较为简单,不再赘述。

(四)资源监控模块
function sendData(data) {
  console.log('我才不要每次都触发呢',data);
    // 在这里,您可以使用AJAX、Fetch或其他方法将数据发送到服务器
    // 例如:
    // fetch('/api/track', {
    //   method: 'POST',
    //   body: JSON.stringify(data),
    //   headers: {
    //     'Content-Type': 'application/json'
    //   }
    // });
}
  // 获取资源性能数据
  var resourceData = performance.getEntriesByType('resource');

  // 遍历资源数据
  resourceData.forEach(function(resource) {
    // 获取资源的相关信息,例如名称、类型、大小等
    let name = resource.name;
    let type = resource.initiatorType;
    let size = resource.transferSize;
    
    // 可计算的资源时间
    console.log(`== 资源 [${i}] - ${resource.name}`);
    // 重定向时间
    let t = resource.redirectEnd - resource.redirectStart;
    console.log(`… 重定向时间 = ${t}`);

    // DNS时间
    t = resource.domainLookupEnd - resource.domainLookupStart;
    console.log(`… DNS查询时间 = ${t}`);

    // TCP握手时间
    t = resource.connectEnd - resource.connectStart;
    console.log(`… TCP握手时间 = ${t}`);

    // 响应时间
    t = resource.responseEnd - resource.responseStart;
    console.log(`… 响应时间 = ${t}`);

    // 获取直到响应结束
    t =
      resource.fetchStart > 0 ? resource.responseEnd - resource.fetchStart : "0";
    console.log(`… 获取直到响应结束时间 = ${t}`);

    // 请求开始直到响应结束
    t =
      resource.requestStart > 0
        ? resource.responseEnd - resource.requestStart
        : "0";
    console.log(`… 请求开始直到响应结束时间 = ${t}`);

    // 开始直到响应结束
    t =
      resource.startTime > 0 ? resource.responseEnd - resource.startTime : "0";
    console.log(`… 开始直到响应结束时间 = ${t}`);
  });
    // 构造要发送的资源数据
    var resData = {
      type: 'resource',
      name: name,
      resourceType: type,
      size: size,
      // 其他您想要收集的信息
    };

    // 发送资源数据
    sendData(resData);
  });
复制代码

resourceData参数如图所示,可以看见内部含有initiatorType(类型),transferSize(大小等)等参数。

image.png

三、使用以及优化

不知大家发现了没有,上面的监控代码冗余,每个模块内部均在使用sendData方法,这里其实是可以抽离出来的,而且在用户行为监控时:“input”, "scroll"事件一直在频繁触发,又或者是使sendData方法进行上送参数时,可以不用那么频繁,是不是可以采用定时发送呢?针对error模块内部,也可以劫持console.error,进行统一上报处理,所以针对这些问题我们接下来的代码主要进行这些优化:

  • 抽离sendData方法,采用多个文件之间相互引用的方法。
  • 对户行为监控的特定事件做一些防抖、节流的处理。
  • 优化sendData方法,采用定时发送。
  • error模块内部,也可以劫持console.error,进行统一上报处理。

更改过后各个模块的代码如下

性能监控模块

性能监控模块成为入口模块。在该模块中引入其他模块,在入口模块处使用sendData函数,并增加缓存数组,定时发送功能。

// 引入其他模块
import './resource.js'
import './error.js'
import './event.js'

// 定义一个缓存数组
var cache = [];

// 定义一个发送数据的函数
function sendData(data) {
  // 将数据添加到缓存数组中
  cache.push(data);
}

// 定义一个定时发送数据的函数
function sendCache() {
  // 判断缓存数组是否为空
  if (cache.length > 0) {
    // 在这里,您可以使用AJAX、Fetch或其他方法将数据发送到服务器
    // 例如:
    // fetch('/api/track', {
    //   method: 'POST',
    //   body: JSON.stringify(cache),
    //   headers: {
    //     'Content-Type': 'application/json'
    //   }
    // });

    // 清空缓存数组
    cache = [];
  }
}

// 启动定时器,每隔一段时间执行一次sendCache函数
setInterval(sendCache, 10000); // 每隔10秒执行一次

// 监听页面加载事件
window.addEventListener('load', function() {
  // 获取性能数据
  const [performanceData] = performance.getEntriesByType("navigation");
  // 即将废弃 推荐上面的PerformanceNavigationTiming写法
  // var performanceData = window.performance.timing;
  // 计算页面加载时间
  console.log('performanceData',performanceData);
  var pageLoadTime = performanceData.domContentLoadedEventEnd - performanceData.navigationStart;
  // 计算请求响应时间
  const requestResponseTime = performanceData.responseEnd - performanceData.requestStart;

  // 计算DNS查询时间
  var dnsLookupTime = performanceData.domainLookupEnd - performanceData.domainLookupStart;

  // 计算TCP连接时间
  var tcpConnectTime = performanceData.connectEnd - performanceData.connectStart;

  // 计算白屏时间
  var whiteScreenTime = performanceData.responseStart - performanceData.navigationStart;
  // 获取 FCP 时间
  let fcpTime = 0;
  const [fcpEntry] = performance.getEntriesByName("first-contentful-paint");
  if (fcpEntry) {
    fcpTime = fcpEntry.startTime;
  }

  // 获取 LCP 时间
  let lcpTime = 0;
  const lcpEntries = performance.getEntriesByType("largest-contentful-paint");
  if (lcpEntries.length > 0) {
    lcpTime = lcpEntries[lcpEntries.length - 1].renderTime || lcpEntries[lcpEntries.length - 1].loadTime;
  }
  // 构造要发送的性能数据
  var perfData = {
    type: 'performance',
    pageLoadTime: pageLoadTime,
    dnsLookupTime: dnsLookupTime,
    tcpConnectTime: tcpConnectTime,
    whiteScreenTime: whiteScreenTime,
    requestResponseTime: requestResponseTime,
    // 其他您想要收集的信息
  };

  // 发送性能数据
  sendData(perfData);

});

复制代码
error模块

对错误进行统一处理


// 定义一个发送数据的函数


// 监听错误事件
window.addEventListener('error', function(event) {
  // 获取错误信息
  var message = event.message;
  var filename = event.filename;
  var lineno = event.lineno;
  var colno = event.colno;

  // 构造要发送的数据
  var data = {
    type: 'error',
    message: message,
    filename: filename,
    lineno: lineno,
    colno: colno,
    // 其他您想要收集的信息
  };

  // 发送数据
  sendData(data);
});
// 劫持console.error
const originConsoleError = console.error;
// 上报每个error
console.error = (...errors)=>{
  errors.forEach((e) => {
       handleError(e) // 处理错误并上报emit
  } );
  originConsoleError.apply(console, errors);
};
复制代码
用户行为模块

增加节流、防抖函数。

// 防抖函数
function debounce(fn, delay) {
  let timer = null
  return function () {
    if (timer !== null) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.call(this)
    }, delay)
  }
}
// 节流函数
function throttle(fn, delay) {
  let flag = true;
  return function () {
    if (flag) {
      setTimeout(() => {
        fn.call(this)
        flag = true
      }, delay)
    }
    flag = false
  }
}

// 监听点击事件
document.addEventListener('click', function(event) {
  // 获取点击的元素
  var target = event.target;
  console.log('我点击了',event);

  // 获取元素的相关信息,例如ID、类名等
  var id = target.id;
  var className = target.className;

  // 构造要发送的数据
  var data = {
    type: 'click',
    id: id,
    className: className,
    // 其他您想要收集的信息
  };

  // 发送数据
  sendData(data);
});

// 监听滚动事件
document.addEventListener('scroll', throttle(function(event) {
  // 获取滚动位置
  var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

  // 构造要发送的数据
  var data = {
    type: 'scroll',
    scrollTop: scrollTop,
    // 其他您想要收集的信息
  };

  // 发送数据
  sendData(data);
},2000));

// 监听输入事件
document.addEventListener('input', debounce(function(event) {
  // 获取输入的元素和值
  var target = event.target;
  var value = target.value;

  // 构造要发送的数据
  var data = {
    type: 'input',
    value: value,
    // 其他您想要收集的信息
  };

  // 发送数据
  sendData(data);
},2000));
复制代码
四、还有什么要做的和要注意的

在实现前端监控时,需要考虑如何避免影响用户体验、如何保证数据的准确性和完整性。下面是一些可能有用的建议:

  • 避免影响用户体验:在实现前端监控时,应尽量避免对用户体验造成影响。例如,可以使用异步方法来发送数据,以避免阻塞页面的加载和渲染。此外,还可以考虑使用节流和防抖技术来减少数据发送的频率,以减少对网络带宽的占用。

  • 保证数据的准确性:为了保证收集到的数据的准确性,应该尽量避免在收集和发送数据的过程中出现错误。例如,可以使用try-catch语句来捕获可能出现的异常,并在出现异常时进行相应的处理。

  • 保证数据的完整性:为了保证收集到的数据的完整性,应该尽量确保所有需要收集的数据都能够被正确地获取并发送到服务器。例如,您可以使用队列来缓存需要发送的数据,并在网络连接恢复正常时再将其发送出去。


补充一个异步资源加载监控的使用方法

// 这是一个使用 `PerformanceObserver` API 的例子:
// 定义 callback 函数
function callback(list) {
  const entries = list.getEntries();
  for (const entry of entries) {
    console.log(`Resource ${entry.name} loaded in ${entry.duration}ms`);
  }
}

// 创建 PerformanceObserver 对象
const observer = new PerformanceObserver(callback);

// 开始监听 resource 类型的性能条目
observer.observe({ entryTypes: ['resource'] });
复制代码

在这个例子中,我们定义了一个 callback 函数,用来处理新的性能条目。在这个函数中,我们遍历了所有新的性能条目,并打印了它们的 nameduration 属性。

然后,我们创建了一个 PerformanceObserver 对象,并传递了 callback 函数作为构造函数的第一个参数。最后,我们调用了 observe 方法来开始监听 resource 类型的性能条目。

PerformanceObserver 是一个用来观察浏览器性能时间线的接口。它可以用来监听不同类型的性能条目,例如 resourcelongtaskpaint 等。当有新的性能条目被记录时,会调用指定的回调函数。

当有新的 resource 类型的性能条目被记录时,会调用 callback 函数,并将新的性能条目作为参数传递给该函数。在这个例子中,我们打印了每个资源的名称和加载时间。

ps:这些只是一些简单的建议,实际应用中可能会更加复杂。您需要根据您的项目实际情况进行相应的调整和修改。我只写了部分优化,勿喷~~

就这些吧,欢迎各位前辈指点~~, 期待共同进步!

猜你喜欢

转载自juejin.im/post/7219669812158414903