perf-observer-kit
Version:
A lightweight, flexible library for monitoring web performance metrics including Core Web Vitals, resource loading performance, long tasks, and navigation timing.
277 lines • 10.9 kB
JavaScript
import { BaseObserver } from './base-observer';
import { logger } from '../../utils/logger';
/**
* Cumulative Layout Shift (CLS) 观察者
* 负责测量页面布局稳定性
* 使用 Google Web Vitals 推荐的会话窗口计算方法
*/
export class CLSObserver extends BaseObserver {
constructor(options) {
super(options);
this.clsObserver = null;
// 会话窗口相关属性
this.sessionCount = 0; // 当前会话中的偏移数量
this.sessionValues = [0]; // 各个会话窗口的CLS值
this.sessionGap = 1000; // 会话间隔时间,单位毫秒
this.sessionMax = 5; // 最大会话窗口数量
this.maxSessionEntries = 100; // 每个会话记录的最大偏移数量
// 上次报告的CLS值
this.prevReportedValue = 0;
// 上次偏移的时间戳
this.lastEntryTime = 0;
// 是否需要在页面重新可见时重置会话
this.shouldResetOnNextVisible = false;
// 防抖计时器
this.reportDebounceTimer = null;
// 防抖延迟
this.reportDebounceDelay = 500;
// 初始化首次隐藏时间
this.firstHiddenTime = this.initFirstHiddenTime();
// 监听visibility变化以更新首次隐藏时间
this.setupFirstHiddenTimeListener();
}
/**
* 获取页面首次隐藏的时间
*/
initFirstHiddenTime() {
// 如果页面已经是隐藏状态,返回0
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
return 0;
}
// 否则返回无限大,表示页面尚未隐藏
return Infinity;
}
/**
* 设置监听页面首次隐藏的事件
*/
setupFirstHiddenTimeListener() {
if (typeof document === 'undefined')
return;
const updateHiddenTime = () => {
if (document.visibilityState === 'hidden' && this.firstHiddenTime === Infinity) {
this.firstHiddenTime = performance.now();
logger.debug(`记录页面首次隐藏时间: ${this.firstHiddenTime}ms`);
}
};
// 监听页面visibility变化
document.addEventListener('visibilitychange', updateHiddenTime, { once: true });
// 页面卸载时也视为隐藏
document.addEventListener('unload', updateHiddenTime, { once: true });
}
/**
* 为CLS指标分配评级
*/
assignCLSRating(value) {
if (value <= CLSObserver.CLS_GOOD_THRESHOLD) {
return 'good';
}
else if (value <= CLSObserver.CLS_NEEDS_IMPROVEMENT_THRESHOLD) {
return 'needs-improvement';
}
else {
return 'poor';
}
}
/**
* 实现观察CLS的方法
*/
observe() {
if (typeof PerformanceObserver === 'undefined') {
logger.error('PerformanceObserver API不可用,无法监控CLS');
return;
}
try {
this.clsObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
// 只处理页面在可见状态时发生的布局偏移
if (document.visibilityState !== 'visible') {
logger.debug('页面不可见,忽略布局偏移事件');
return;
}
// 处理新会话的标志
let newSessionStarted = false;
for (const entry of entries) {
// 只计算用户未操作时的布局偏移
const layoutShift = entry;
if (!layoutShift.hadRecentInput && layoutShift.startTime < this.firstHiddenTime) {
// 获取偏移发生的时间戳
const currentTime = layoutShift.startTime;
// 判断是否需要开始新会话
if (this.shouldResetOnNextVisible || currentTime - this.lastEntryTime > this.sessionGap) {
this.startNewSession(layoutShift.value);
newSessionStarted = true;
this.shouldResetOnNextVisible = false;
}
else {
// 累加到当前会话,但限制记录的事件数量
if (this.sessionCount < this.maxSessionEntries) {
this.sessionCount++;
// 累加到当前会话
const currentSessionIndex = this.sessionValues.length - 1;
this.sessionValues[currentSessionIndex] += layoutShift.value;
}
}
// 更新最后一次偏移时间
this.lastEntryTime = currentTime;
// 防抖处理,减少频繁报告
this.debouncedReportCLS();
}
}
// 如果开始了新会话,立即报告一次,不需要防抖
if (newSessionStarted) {
this.reportCLS(this.calculateCLS());
}
});
// 使用buffered选项确保不会丢失之前的布局偏移
this.clsObserver.observe({ type: 'layout-shift', buffered: true });
logger.debug('CLS观察者已启动,开始监控布局偏移');
}
catch (error) {
logger.error('CLS监控不受支持', error);
}
}
/**
* 开始新的会话窗口
* @param initialValue 初始偏移值
*/
startNewSession(initialValue) {
// 添加新会话
this.sessionValues.push(initialValue);
this.sessionCount = 1;
// 如果会话窗口超过限制,移除最小的会话
if (this.sessionValues.length > this.sessionMax) {
// 找到最小的会话值及其索引
let minValue = Infinity;
let minIndex = 0;
for (let i = 0; i < this.sessionValues.length; i++) {
if (this.sessionValues[i] < minValue) {
minValue = this.sessionValues[i];
minIndex = i;
}
}
// 移除最小的会话
this.sessionValues.splice(minIndex, 1);
}
logger.debug(`开始新的CLS会话,当前会话数: ${this.sessionValues.length}`);
}
/**
* 防抖报告CLS,减少频繁更新
*/
debouncedReportCLS() {
// 清除现有计时器
if (this.reportDebounceTimer !== null) {
window.clearTimeout(this.reportDebounceTimer);
}
// 设置新计时器
this.reportDebounceTimer = window.setTimeout(() => {
const clsValue = this.calculateCLS();
// 只有当CLS值显著变化时才报告
if (Math.abs(clsValue - this.prevReportedValue) >= 0.01) {
this.reportCLS(clsValue);
}
this.reportDebounceTimer = null;
}, this.reportDebounceDelay);
}
/**
* 计算最终CLS值
* 取所有会话窗口中的最大值
*/
calculateCLS() {
return Math.max(...this.sessionValues);
}
/**
* 报告CLS指标
*/
reportCLS(clsValue) {
const cls = {
name: 'CLS',
value: clsValue,
unit: '', // CLS没有单位,是无量纲数值
timestamp: new Date().getTime(),
url: typeof window !== 'undefined' ? window.location.href : undefined,
// 添加网络信息和其他上下文
context: {
shiftCount: this.sessionCount,
sessionCount: this.sessionValues.length,
sessionValues: [...this.sessionValues],
largestSession: this.calculateCLS(),
isPageVisible: document.visibilityState === 'visible',
firstHiddenTime: this.firstHiddenTime === Infinity ? null : this.firstHiddenTime
}
};
// 设置评级
cls.rating = this.assignCLSRating(cls.value);
logger.debug(`报告CLS值: ${cls.value},评级: ${cls.rating},页面可见性: ${document.visibilityState}`);
this.onUpdate(cls);
// 更新上次报告的值
this.prevReportedValue = clsValue;
}
/**
* 停止CLS观察
*/
stop() {
// 清除防抖计时器
if (this.reportDebounceTimer !== null) {
window.clearTimeout(this.reportDebounceTimer);
this.reportDebounceTimer = null;
}
if (this.clsObserver) {
this.clsObserver.disconnect();
this.clsObserver = null;
}
super.stop();
}
/**
* 页面可见性变化时的回调
* @param isVisible 页面是否可见
*/
onVisibilityChange(isVisible) {
if (!isVisible) {
// 页面隐藏时,报告当前的CLS值
if (this.firstHiddenTime === Infinity) {
this.firstHiddenTime = performance.now();
logger.debug(`页面隐藏,更新firstHiddenTime: ${this.firstHiddenTime}ms`);
}
const clsValue = this.calculateCLS();
// 无论大小变化,都在页面隐藏时报告一次
this.reportCLS(clsValue);
// 标记需要在页面重新可见时开始新会话
this.shouldResetOnNextVisible = true;
}
else {
// 页面重新变为可见
if (this.shouldResetOnNextVisible) {
logger.debug('页面重新可见,准备开始新的CLS会话');
// 实际的重置会在下一个布局偏移发生时生效
}
}
}
/**
* BFCache恢复处理
* CLS应该重置累积值
*/
onBFCacheRestore(event) {
// 重置CLS会话值
this.sessionValues = [0];
this.sessionCount = 0;
this.prevReportedValue = 0;
this.lastEntryTime = 0;
// 清除计时器
if (this.reportDebounceTimer !== null) {
window.clearTimeout(this.reportDebounceTimer);
this.reportDebounceTimer = null;
}
// 重置firstHiddenTime
this.firstHiddenTime = this.initFirstHiddenTime();
this.setupFirstHiddenTimeListener();
logger.info('CLS值已在bfcache恢复后重置');
// 重新开始CLS监测
if (this.clsObserver) {
this.clsObserver.observe({ type: 'layout-shift', buffered: true });
}
}
}
// CLS评分阈值
CLSObserver.CLS_GOOD_THRESHOLD = 0.1;
CLSObserver.CLS_NEEDS_IMPROVEMENT_THRESHOLD = 0.25;
//# sourceMappingURL=cls-observer.js.map