JavaScript内核与高级编程之:`JavaScript`的`PerformanceObserver`:如何使用它监听性能指标,如 `LCP` 和 `CLS`。

各位靓仔靓女们,晚上好!我是你们的老朋友,今天咱们来聊聊JavaScript里一个相当给力,但可能被不少人忽略的家伙——PerformanceObserver。这玩意儿就像是性能监控界的007,专门负责监听各种性能指标,比如LCP(Largest Contentful Paint)和CLS(Cumulative Layout Shift)。有了它,咱们就能像医生诊断病情一样,了解网页的健康状况,然后对症下药,让网站跑得飞起来。

准备好了吗?咱们这就开始一场性能监控的探险之旅!

Part 1: PerformanceObserver 是个啥?

想象一下,你是个侦探,需要调查一起“网站性能下降”的案件。你不可能一直盯着屏幕,手动记录各种数据。这时,PerformanceObserver就派上用场了。它就像一个自动化的记录仪,能监听特定的性能事件,并在事件发生时通知你。

简单来说,PerformanceObserver是一个接口,允许你异步地监听性能度量事件。它不会阻塞主线程,所以不会对性能产生额外的负担。

Part 2: PerformanceObserver 的基本用法

PerformanceObserver的使用分为三步:

  1. 创建观察者: 就像招募侦探一样,你需要创建一个PerformanceObserver实例,并告诉它你感兴趣的事件类型。
  2. 定义回调函数: 这是侦探接收线索的地方。当监听的事件发生时,回调函数会被调用,你可以在这里处理性能数据。
  3. 开始观察: 就像启动侦探的监听设备一样,你需要调用observe()方法,指定要观察的事件类型。

让我们用代码来演示一下:

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log(entry.name, entry.startTime, entry.duration);
  });
});

observer.observe({ entryTypes: ['paint', 'measure'] });

这段代码做了什么?

  • new PerformanceObserver((list) => { ... }): 创建了一个新的PerformanceObserver实例。回调函数接收一个PerformanceObserverEntryList对象,里面包含了所有观察到的性能事件。
  • list.getEntries().forEach((entry) => { ... }): 从PerformanceObserverEntryList中获取所有性能事件条目,并遍历它们。
  • console.log(entry.name, entry.startTime, entry.duration): 在控制台打印出每个事件的名称、开始时间和持续时间。
  • observer.observe({ entryTypes: ['paint', 'measure'] }): 告诉PerformanceObserver开始监听paint(绘制)和measure(测量)类型的事件。

Part 3: LCP (Largest Contentful Paint) 监听

LCP代表“最大内容绘制”,衡量的是页面上最大的可见元素完成渲染的时间。它是一个重要的用户体验指标,直接影响用户对页面加载速度的感知。

要监听LCP,我们需要将entryTypes设置为largest-contentful-paint

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log('LCP:', entry.startTime, entry.size, entry.url);
  });
});

observer.observe({ type: 'largest-contentful-paint', buffered: true });

这段代码中,buffered: true表示获取浏览器已经记录的LCP事件,确保我们能捕获到页面加载过程中的LCP。

输出的entry对象包含以下关键信息:

  • startTime: LCP发生的时间戳。
  • size: 最大内容元素的大小(字节)。
  • url: 如果最大内容元素是图片或视频,则这是资源的URL。
  • element: 指向LCP元素的DOM节点。

有了这些数据,我们就能分析LCP过慢的原因,例如:

  • 图片太大:优化图片大小,使用CDN加速。
  • 服务器响应慢:优化服务器性能,使用缓存。
  • 阻塞渲染的JavaScript:延迟加载非关键JavaScript。

Part 4: CLS (Cumulative Layout Shift) 监听

CLS代表“累积布局偏移”,衡量的是页面上元素意外移动的程度。过高的CLS会让用户感到烦躁,影响用户体验。

要监听CLS,我们需要将entryTypes设置为layout-shift

let clsValue = 0;
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
      console.log('Current CLS value:', clsValue, entry);
    }
  }
});

observer.observe({ type: 'layout-shift', buffered: true });

// 页面卸载时发送最终CLS值
window.addEventListener('beforeunload', () => {
    console.log('Final CLS value:', clsValue);
});

这段代码中,entry.value表示单次布局偏移的分数。entry.hadRecentInput 表示偏移是否由用户输入引起的(比如用户点击按钮导致的布局变化),如果是用户输入引起的,就不计入CLS。我们需要累加所有非用户输入引起的布局偏移,得到最终的CLS值。

输出的entry对象包含以下关键信息:

  • value: 布局偏移的分数。
  • startTime: 布局偏移发生的时间戳。
  • sources: 导致布局偏移的元素列表。

有了这些数据,我们就能找出导致CLS的原因,例如:

  • 图片没有指定尺寸:为图片添加widthheight属性,或者使用CSS的aspect-ratio属性。
  • 广告或嵌入式内容:预留足够的空间,避免加载后挤压其他元素。
  • 字体闪烁:使用font-display: swapfont-display: optional来避免字体加载导致的布局偏移。

Part 5: 更多高级用法

PerformanceObserver还有一些更高级的用法,可以帮助我们更深入地了解网站的性能:

  • 监听自定义事件: 可以使用Performance.mark()Performance.measure()来标记和测量自定义事件,然后用PerformanceObserver监听这些事件。

    performance.mark('start');
    // ... 一些代码 ...
    performance.mark('end');
    performance.measure('my-custom-event', 'start', 'end');
    
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        console.log('Custom event:', entry.name, entry.startTime, entry.duration);
      });
    });
    
    observer.observe({ entryTypes: ['measure'] });
  • 过滤事件: 可以使用filter选项来过滤特定类型的事件。

    observer.observe({
      entryTypes: ['resource'],
      buffered: true,
      filter: {
        initiatorType: 'img', // 只监听图片资源
      },
    });
  • disconnect() 方法: 调用disconnect()方法可以停止观察者监听事件。

    observer.disconnect();

Part 6: 实战演练:打造一个简单的性能监控工具

现在,让我们把学到的知识应用到实践中,打造一个简单的性能监控工具。这个工具可以收集LCP和CLS数据,并将它们发送到服务器进行分析。

// 配置
const config = {
  reportURL: '/api/performance', // 上报数据的接口
  lcpThreshold: 2500, // LCP阈值(毫秒)
  clsThreshold: 0.1, // CLS阈值
};

// 辅助函数:发送数据到服务器
function reportToAnalytics(metric) {
  const data = {
    name: metric.name,
    value: metric.value,
    details: metric.details || {},
  };

  if (navigator.sendBeacon) {
    navigator.sendBeacon(config.reportURL, JSON.stringify(data));
  } else {
    fetch(config.reportURL, {
      method: 'POST',
      body: JSON.stringify(data),
      keepalive: true, // 确保在页面卸载后也能发送数据
    });
  }
}

// LCP 监听
let lcpValue = null;
const lcpObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    lcpValue = entry; // 记录最新的LCP值
    console.log('LCP:', entry.startTime, entry.size, entry.url);

    if (entry.startTime > config.lcpThreshold) {
      reportToAnalytics({
        name: 'LCP',
        value: entry.startTime,
        details: {
          size: entry.size,
          url: entry.url,
        },
      });
      lcpObserver.disconnect(); // 达到阈值后停止监听
    }
  });
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });

// CLS 监听
let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
      console.log('Current CLS value:', clsValue, entry);
      if (clsValue > config.clsThreshold) {
        reportToAnalytics({
          name: 'CLS',
          value: clsValue,
          details: {
            entries: list.getEntries().map((e) => ({
              value: e.value,
              startTime: e.startTime,
              sources: e.sources,
            })),
          },
        });
        clsObserver.disconnect(); // 达到阈值后停止监听
      }
    }
  }
});
clsObserver.observe({ type: 'layout-shift', buffered: true });

// 页面卸载时发送最终LCP和CLS值
window.addEventListener('beforeunload', () => {
    if (lcpValue) {
        reportToAnalytics({
            name: 'LCP',
            value: lcpValue.startTime,
            details: {
              size: lcpValue.size,
              url: lcpValue.url,
            },
        });
    }

    if (clsValue > 0) {
        reportToAnalytics({
            name: 'CLS',
            value: clsValue,
        });
    }
});

这个工具做了以下事情:

  1. 配置: 定义了上报数据的接口和LCP/CLS的阈值。
  2. reportToAnalytics() 函数: 负责将性能数据发送到服务器。使用了navigator.sendBeaconfetch API,确保数据在页面卸载后也能成功发送。
  3. LCP 监听: 监听LCP事件,并在LCP超过阈值时上报数据。
  4. CLS 监听: 监听CLS事件,并在CLS超过阈值时上报数据。
  5. 页面卸载时发送最终数据: 在页面卸载时,发送最终的LCP和CLS值,确保所有数据都被捕获。

Part 7: 注意事项和最佳实践

  • 性能开销: 虽然PerformanceObserver是异步的,但监听过多的事件仍然会产生一定的性能开销。只监听你真正需要的事件。
  • 浏览器兼容性: PerformanceObserver的兼容性良好,但仍然建议进行polyfill,以支持旧版本的浏览器。
  • 数据采样: 在高流量的网站上,可以考虑对数据进行采样,以减少服务器的压力。
  • 结合其他工具: PerformanceObserver可以与其他性能分析工具(如Lighthouse、WebPageTest)结合使用,提供更全面的性能分析。
  • 错误处理: 确保你的代码能处理各种错误情况,比如网络错误、数据解析错误等。

表格总结:关键属性和方法

属性/方法 描述
PerformanceObserver 构造函数,创建一个新的性能观察者。
observe(options) 开始监听指定类型的性能事件。options对象可以包含entryTypestypebufferedfilter等属性。
disconnect() 停止观察者监听事件。
getEntries() 返回所有观察到的性能事件条目。
getEntriesByName(name, entryType) 返回指定名称和类型的性能事件条目。
getEntriesByType(entryType) 返回指定类型的性能事件条目。
entry.name 性能事件的名称。
entry.startTime 性能事件的开始时间戳。
entry.duration 性能事件的持续时间(毫秒)。
entry.size LCP元素的大小(字节)。
entry.url 如果LCP元素是图片或视频,则这是资源的URL。
entry.value CLS的布局偏移分数。
entry.sources 导致布局偏移的元素列表。

结语

好了,今天的PerformanceObserver之旅就到这里了。希望通过今天的学习,大家能够掌握PerformanceObserver的基本用法,并能利用它来监控网站的性能,优化用户体验。记住,性能优化是一个持续不断的过程,需要我们不断地学习和实践。

祝大家都能成为性能优化的大师! 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注