当生产环境出现性能或流程阻塞问题时,往往会引起用户投诉。开发人员需要一套能够持续监控性能和异常的页面监控系统,提前发现线上异常问题,快速定位并解决。
1. 性能监控
页面性能直接影响用户的实际体验,也是开发人员需要重点关注的部分。研究表明,用户最满意的打开网页时长是2~5s,如果等待超过10s,多数用户会直接退出页面。
页面性能是由多方面因素共同决定的,开发人员在开发环境下能快速访问页面并不意味着用户在生产环境中也能如此。如果想要衡量页面的性能,就需要建立一套客观衡量的指标,并基于这些指标完善监控系统。
1.1 Performance API
W3C 在2010年成立了Web Performance Working Group,提供了可以衡量页面性能的API——window.performance
API。
window.performance
会返回一个Performance类型的对象,其中performance.timing
包含了各种与浏览器性能有关的时间数据,提供浏览器各处理阶段的耗时。浏览器加载页面的过程被分为9个阶段,并且在window.performance
中提供了对应的时间戳标记:
每个阶段的关键节点时间戳的含义具体如下:
-
unload
- unloadEventStart:旧页面开始卸载页面资源的时间点,
unload
事件抛出时的时间戳。如果没有上一个文档,或者重定向中的一个不同源,这个值会返回0。 - unloadEventEnd:旧页面完成所有资源卸载的时间点,
unload
事件处理完成时的 时间戳。如果没有上一个文档,或者重定向中的一个不同源,这个值会返回0。
- unloadEventStart:旧页面开始卸载页面资源的时间点,
-
navigationStart(导航开始):同一个浏览器上下文的上一个文档卸载(
unload
)结束时的时间戳。如果没有上一个文档,这个值会和PerformanceTiming.fetchStart
相同。 -
redirect
- redirectStart(重定向开始):当页面加载过程中发生重定向时,这个时间戳标记了重定向操作开始的时刻。
- redirectEnd(重定向结束):重定向操作完成的时间,即最后一个 HTTP 重定向完成时的时间戳。
-
fetchStart(获取开始):表示浏览器准备好使用 HTTP 请求来获取(fetch)文档的时间点。
-
DNS
- domainLookupStart(域名查询开始):这个时间戳标记了浏览器开始进行DNS查询的时刻。如果使用了持久连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和
PerformanceTiming.fetchStart
一致。 - domainLookupEnd(域名查询结束):浏览器完成域名查询的时间。
- domainLookupStart(域名查询开始):这个时间戳标记了浏览器开始进行DNS查询的时刻。如果使用了持久连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和
-
connect
- connectStart(连接开始):浏览器开始尝试与服务器建立连接的时刻。这个连接可能是基于 HTTP 或 HTTPS 协议的 TCP 连接,TCP连接开始的时间就是
connectStart
。如果使用持久连接,则返回值等同于 fetchStart 属性的值。 - secureConnectionStart:浏览器与服务器开始安全连接握手时的时间戳。如果当前网页不要求安全连接,则返回 0。
- connectEnd(连接结束):成功建立连接的时间。这个阶段的时间包括了 TCP 三次握手等建立连接的过程,连接的速度受到网络带宽、服务器负载以及网络拥塞等因素的影响。
- connectStart(连接开始):浏览器开始尝试与服务器建立连接的时刻。这个连接可能是基于 HTTP 或 HTTPS 协议的 TCP 连接,TCP连接开始的时间就是
-
requestStart(请求开始):浏览器向服务器发出 HTTP 请求时(或开始读取本地缓存时)的时间戳。
-
response
- responseStart(响应开始):服务器开始发送资源响应的时间。当服务器接收到浏览器的请求后,开始准备资源并发送给浏览器,这个发送的起始时间就是
responseStart
。它标志着服务器已经处理完请求的前期准备工作,开始将资源数据传输回浏览器。 - responseEnd(响应结束):服务器完成资源响应的时间,也就是浏览器接收完所有请求的资源的时间。这个阶段的时间长短取决于资源的大小、服务器的带宽以及网络状况等因素。
- responseStart(响应开始):服务器开始发送资源响应的时间。当服务器接收到浏览器的请求后,开始准备资源并发送给浏览器,这个发送的起始时间就是
-
processing
- domLoading:网页 DOM 结构开始解析的时间戳,即
document.readyState
属性变为loading
、相应的readystatechange
事件触发时。它是页面加载过程中的一个重要里程碑,因为后续的许多操作(如加载外部资源、执行脚本等)都依赖于 DOM 树的构建。 - domInteractive:网页 DOM 结构解析完成、开始加载内嵌资源的时间戳,即
document.readyState
属性变为“interactive”、相应的readystatechange
事件触发时。在这个阶段,用户可以开始与页面进行一些交互,但页面可能尚未完全加载完成。对于需要尽快响应用户交互的页面,这个时间点至关重要。 - domContentLoadedEventStart:所有需要被执行的脚本已经被解析,解析器发送
DOMContentLoaded
事件时触发。它反映了页面核心内容的加载速度,对于优化页面的核心内容加载有重要的参考价值。 - domContentLoadedEventEnd:所有需要立即执行的脚本已经被执行时触发。它标志着页面核心内容加载后的一个稳定阶段,页面在核心内容层面已经完全就绪。
- domComplete:当前文档解析完成,即
document.readyState
变为complete
且相对应的readystatechange
被触发时的时间戳。此时,页面处于完整状态,所有资源都已就绪,用户可以正常地与页面进行各种交互操作。
- domLoading:网页 DOM 结构开始解析的时间戳,即
-
onLoad
- loadEventStart(加载事件开始):浏览器开始触发
load
事件的时间。此时,页面的 DOM 结构已经构建完成,JavaScript 脚本也已经执行了大部分的初始化操作,页面在视觉上基本已经加载完成。 - loadEventEnd(加载事件结束):
load
事件执行完成的时间。这个时间点之后,页面进入完全可用状态,用户可以与页面进行交互,并且所有的资源加载相关的脚本和操作都已经完成。对于一些需要在页面完全加载后才能执行的功能,这个阶段是一个重要的参考时间。
- loadEventStart(加载事件开始):浏览器开始触发
通过以上的时间戳可以计算每个阶段的耗时,从而建立更直观的指标。比如:
- 重定向耗时:
redirectEnd
-redirectStart
- DNS解析耗时:
domainLookupEnd
-domainLookupStart
- TCP连接耗时:
connectEnd
-connectStart
- SSL安全连接耗时:
connectEnd
-secureConnectionStart
此外,performance
还提供了 getEntries
方法,它会返回一个 PerformanceEntry
对象数组,用于记录浏览器的绘制、资源加载等行为,可以借助它获取一些更复杂的指标。
1.2 核心性能指标
Google提供了很多性能测量和性能报告工具,可以用于评估页面的性能状况,这些工具和指标令人应接不暇。后来,Google启动了Web Vitals
计划,它提供了一组以用户为中心的衡量标准,简化了性能评估的手段,帮助开发人员专注于最重要的指标,即核心性能指标。
构成 Core Web Vitals 的指标会随着时间的推移而演变。当前这组指标侧重于用户体验的三个方面:加载速度、互动性和视觉稳定性,其中包括以下指标(及其各自的阈值):
- Largest Contentful Paint (LCP) :衡量加载性能。为了提供良好的用户体验,应在网页首次开始加载的 2.5 秒内完成 LCP。
- Interaction to Next Paint (INP) :衡量互动性。为了提供良好的用户体验,网页的 INP 应不超过 200 毫秒。
- Cumulative Layout Shift (CLS) :衡量视觉稳定性。为了提供良好的用户体验,网页的 CLS 应保持在 0.1 或更低。
除了使用开发者工具之外,还可以使用Chrome开源的 web-vitals 工具库来衡量核心网页指标。
1.3 其他指标
除了核心性能指标,还有其他指标也可以帮助开发人员有效衡量性能状况。它们可以作为核心性能指标的补充,帮助开发人员获取更多信息,排查特定问题。比如:
-
TTFB(Time To First Byte):首次发送字节时间,衡量请求资源到响应第一个字节开始到达之间的时间,即从
redirectStart
到responseStart
之间的经过时间。它反映了服务器的响应速度和网络延迟情况。 -
FCP(First Contentful Paint):首次内容绘制时间,衡量从用户首次导航到网页到网页任何一部分内容(文本、图片、背景图片、
<svg>
元素或非白色<canvas>
元素等)呈现在屏幕上的时间。它至少包括了从navigationStart
到responseStart
之间的时间。它和另一个指标 LCP(Largest Contentful Paint) 的区别在于,后者旨在衡量网页的主要内容何时加载完毕。FCP 能够直观地反映出用户首次看到页面内容的时间,优化 FCP 可以让用户更快地感知到页面的加载进展,减少等待的焦虑感。 -
TBT(Total Blocking Time):总阻塞时间,衡量从 FCP 到可交互时间之间的总时间,这段时间内由于主线程阻塞而无法交互。每当存在长任务(即在主线程上运行超过 50 毫秒的任务)时,主线程都会被视为“阻塞”,长任务的阻塞时间是指其超过 50 毫秒的时长。TBT 是一个实验室指标,对于发现和诊断可能影响 INP 的潜在互动性问题很有帮助。
-
TTI(Time to Interactive):可交互时间,指页面从开始加载到完全可交互的时间。当页面的主线程处于空闲状态,并且页面上的所有关键资源(如样式表、脚本)都已加载并执行完成,且没有长时间的任务阻塞用户交互时,即可认为达到了 TTI。
因为用户交互会影响TBT、TTI的结果,因此不建议在生产环境中检测,一般推荐在开发环境中使用lighthouse
进行测量。如果需要衡量生产环境的可交互性,建议使用 FID 指标代替。
以上可以作为衡量网站性能的通用指标,某些网站存在特殊架构或设计,开发人员需要自定义指标评估其性能。此时,可以结合 performance
API和其他手段,开发自定义指标。
2. 异常监控
代码异常监控的目的是为了发现已存在的代码隐患,根据报错信息对线上问题进行定位和修复。
2.1 异常采集
为了实现异常监控,就要对线上错误进行采集。异常采集的手段包括:
使用浏览器原生的 JavaScript 异常捕获
-
try-catch
捕获异常:在代码中手动对可能出现异常的代码进行包裹,从而捕获异常。它只能捕获当前执行上下文抛出的错误,对于异步场景等脱离当前执行上下文的错误无法捕获。而且对代码侵入性比较大,如果大量使用会带来负担,影响代码可读性。 -
window.onerror
回调:全局监听JavaScript异常,可以捕获页面中未被try-catch
捕获的 JavaScript 错误。但是它无法捕获资源加载的错误。 -
window.addEventListener
监听error
事件:它具备window.onerror
的多数功能,在错误捕获时机上,它比onerror
更早触发。通过设置addEventListener
的第三个参数为true
,还可以在事件捕获阶段采集到错误,从而实现对资源加载错误的监听。可以根据捕获错误的特征来区分它是JavaScript错误还是资源错误:- 使用
lineno
和colno
字段:JavaScript错误有这两个字段,资源错误的值为undefined - 利用
instanceof
来判断错误类型:JavaScript错误的事件类型是ErrorEvent
,资源错误的事件类型是Error
。
- 使用
-
window.addEventListener
监听unhandledrejection
事件:对于没有catch捕获的Promise错误,需要全局监听unhandledrejection
事件来采集。通过event.reason.stack
可以获取错误的具体信息。
通过以上措施,开发人员基本可以采集到JavaScript中的错误。在实际开发中,还要考虑跨域脚本错误捕获的限制。出于安全性考虑,浏览器会阻止外部脚本获取如错误堆栈、具体的变量值等详细信息,此时就无法采集到有效错误信息。为了解决这个问题,可以为跨域脚本添加crossorigin="anonymous"
配置,并且在服务器端配置 CORS 头 Access-Control-Allow-Origin
,这样就可以正常捕获JavaScript错误了。
使用第三方错误监控服务
除了以上方法,还可以使用第三方错误监控服务的SDK进行错误采集。比如使用Sentry
的 JavaScript SDK 即可通过简单的配置实现自动捕获和上报错误,其底层也是基于window.error
重写、全局监听error
事件和unhandledrejection
事件来实现自动错误采集的。此外,还可以使用 SDK 提供的 captureException
或 captureMessage
等方法手动捕获和上报错误。
2.2 错误处理
错误处理可以分三类:同步错误、异步错误和资源加载错误。为了方便后续的错误监控、排查定位工作,开发人员应该为每种错误类型都定义一个固定的数据结构。
- 同步错误和异步错误都属于JavaScript错误,需要关注错误的文件地址和错误堆栈
- 资源加载错误与JavaScript逻辑无关,需要关注资源的加载标签和地址
function parseError(error) {
if (error instanceof ErrorEvent) {
console.log("同步错误:", error.message);
return {
filename: error.filename,
message: error.message,
lineno: error.lineno,
colno: error.colno,
// 抛出的错误为Error时可以读取stack信息,否则直接转换字符串
stack: error instanceof Error ? error.stack : JSON.stringify(error)
};
} else if (error instanceof PromiseRejectionEvent) {
console.log("Promise 错误:", error.reason);
// 通过正则表达式提取stack信息
const { filename, lineno, colno } = parseStack(error.reason.stack)
return {
filename,
lineno,
colno,
message: error.reason.message,
stack: error.reason.stack
};
} else if (error instanceof Event) {
console.log("资源加载错误:", error.message);
const { target } = error;
return {
tag: target.nodeName,
resourceUrl: target.src || target.href,
// 获取dom属性
attrs: getAllAttrs(target)
};
} else {
console.log("其他类型错误:", error);
}
}
2.3 错误排查
开发人员在错误监控采集和处理之后,就需要对收集到的JavaScript 错误信息进行分析,定位问题。但是由于生产环境的代码经过了转换、压缩或混淆处理,错误信息的行列、堆栈对应的是处理后的代码。这时候 SourceMap
就派上用场了。
SourceMap
是一个源码映射工具,可以将压缩后的代码映射回构建前的状态。其原理是通过保存代码处理前后在行、列上的对应关系,形成类似“映射”的结构,生成对应的 SourceMap
文件。借助它可以快速查找到错误信息中的压缩代码对应的原始代码。
注意,由于 SourceMap
文件可以逆向生成处理前的源代码,存在安全风险,所以 SourceMap
文件不允许暴露在生产环境中。为了便于排查问题,开发人员一般会将SourceMap
文件上传到内网服务器中,只允许员工访问,避免代码泄露。开源社区提供了 source-map
工具模块,开发人员经过简单的配置就可以根据错误信息和 SourceMap
文件解析出源代码的报错位置。
3. 页面崩溃监控
当页面出现内存泄露、内存溢出、严重性能问题或者其他不可预测的错误时,都有可能导致其运行的浏览器崩溃。页面崩溃会中断用户正在进行的操作,可能导致用户丢失未保存的数据,降低用户体验,影响用户对应用程序或网站的信任度。因此,我们需要对页面崩溃进行监控处理。
由于崩溃的不可预测性,以及发生崩溃时我们的代码无法执行,浏览器的网络环境也不再正常工作,我们无法在页面崩溃时记录并发送日志。为了实现页面崩溃监控,可以从以下方面考虑:
- 使用本地存储记录页面的生命周期,在页面恢复时检测并发送页面崩溃状态
- 使用
Service Worker
在后台记录并发送 - 使用浏览器的
Reporting API
在页面崩溃时自动上报
下面我们详细看看这几种方案。
3.1 使用本地存储记录页面生命周期
利用页面的生命周期可以检测页面是否正常关闭。
load
:记录页面加载状态beforeunload
:记录页面正常关闭状态。页面崩溃时不会触发这个方法。
因此,我们将生命周期状态记录在本地存储中,在打开页面时检查上次关闭的页面的生命周期记录,如果没有beforeunload
记录,说明发生了页面崩溃。然后我们需要考虑这两种本地存储方式:
-
localStorage
:持久化存储,可以获取上次关闭的页面状态。然而,如果页面同时打开多个tab,它们将会共享localStorage
,造成状态记录的相互覆盖,影响判断。 -
sessionStorage
:通常情况下,sessionStorage
会在页面关闭时被清除。从浏览器的角度来看,页面崩溃类似于页面关闭,因此页面崩溃通常会导致sessionStorage
丢失。但是Chrome 和 Firefox 浏览器支持sessionStorage
持久化的机制,页面重新打开时可以恢复sessionStorage
。因此可以利用它在页面重新打开时检测上次页面崩溃状态。
简单实现如下:
window.addEventListener('load', function () {
// 页面加载:记录页面未卸载状态
sessionStorage.setItem('good_exit', 'pending');
setInterval(function () {
// 在 sessionStorage 中记录心跳
sessionStorage.setItem('time_before_crash', new Date().toString());
}, 1000);
});
window.addEventListener('beforeunload', function () {
// 页面卸载前:记录页面正常卸载状态
sessionStorage.setItem('good_exit', 'true');
});
// 下次页面恢复时:检测页面崩溃状态
function checkLastCrash() {
if(sessionStorage.getItem('good_exit') &&
sessionStorage.getItem('good_exit') !== 'true') {
/*
insert crash logging code here
*/
alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
}
}
使用sessionStorage
也存在一定的局限:
- 浏览器兼容性问题:
sessionStorage
持久化机制依赖于浏览器的实现 - 依赖于页面恢复的操作:只能在原来的tab中恢复页面时才能获取到上次的
sessionStorage
状态,上报的时效不可控。
参考:Logging Information on Browser Crashes
3.2 使用 Service Worker
后台心跳检测
Service Worker
在后台的独立线程中运行,所以当页面崩溃或关闭时,它还可以继续工作。我们可以通过 postMessage
机制在页面和 Service Worker
之间通信,定时发送心跳,当页面崩溃时,Service Worker
没有收到页面的心跳,就可以判断页面崩溃发生并上报。
在页面主线程中发送心跳:
window.addEventListener('load', function() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
startHeartbeat();
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
}
});
window.addEventListener('beforeunload', function() {
stopHeartbeat();
clearInterval(window.heartbeatInterval);
});
function startHeartbeat() {
// 每 5 秒发送一次心跳消息
window.heartbeatInterval = setInterval(() => {
postHeartbeat({ type: 'heartbeat' });
}, 5000);
}
function stopHeartbeat() {
// 停止心跳检测
postHeartbeat({ type: 'unload' });
}
function postHeartbeat(data) {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
...data,
sessionId: '1234567', // 会话id
});
}
}
在Service Worker
中检测心跳:
// service-worker.js
const HEARTBEAT_TIMEOUT = 15000; // 设置心跳超时时间为 15 秒
let heartbeats = {}; // 存储每个页面的心跳时间戳
self.addEventListener('message', function(event) {
if (event.data.type === 'heartbeat') {
heartbeats[event.data.sessionId] = Date.now();
} else if (event.data.type === 'unload') {
delete heartbeats[event.data.sessionId];
}
});
setInterval(checkForCrash, 10000); // 每 10 秒检查一次是否有页面崩溃
function checkForCrash() {
const now = Date.now();
for (let sessionId in heartbeats) {
if (now - heartbeats[sessionId] > HEARTBEAT_TIMEOUT) {
console.log(`Client ${sessionId} might have crashed. Reporting...`);
// 自定义处理,上报崩溃信息
reportCrash();
delete heartbeats[sessionId]; // 删除已报告崩溃的 client 心跳记录
}
}
}
这种方案也存在一定的问题:
-
兼容性:需要检查浏览器是否支持
Service Worker
,而且Service Worker
API 需要在 HTTPS 安全环境中运行。 -
心跳检测问题:在心跳检测时需要根据实际情况找到性能和检测及时性的平衡点。
- 精确度问题:心跳检测依赖于时间间隔,如果时间间隔设置不合理,可能会将页面卡顿误判为页面崩溃
- 性能问题:定时发送心跳消息和检查心跳会带来一定的性能开销,需要避免过于频繁的心跳发送和检查
3.3 使用 Reporting
API自动发送报告
Reporting
API 允许浏览器向服务器自动报告各种错误和问题,包括网络错误、CSP 违规、安全违规以及性能问题等。
为了让浏览器自动发送报告,可以在 HTTP 响应头中或者meta
标签中设置Reporting-Endpoints
:
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader('Reporting-Endpoints', '{"default":{"url":"https://example.com/report-endpoint"}}');
res.end('Hello, World!');
});
server.listen(8080);
当页面崩溃时,浏览器会自动将报告发送到配置的端点。浏览器会发出一个POST 请求,类型为Content-Type: application/reports+json
并带有一个正文,其中包含捕获的警告/错误数组。
使用浏览器提供的自动上报方案大大简化了前端监控的复杂性,但是这种方案也存在一定的问题:
- 浏览器兼容性:
Reporting
API 是实验性API,目前浏览器的支持还处于早期阶段。 - 服务端支持:需要建立一个服务来接收报告
4. 白屏监控
白屏是指在用户浏览的界面中,可见区域内没有任何可浏览的内容。白屏问题严重影响功能流程和用户体验,如果没有及时处理,往往会引起投诉。通过白屏监控,开发和运维团队可以及时发现白屏现象的发生,进而快速定位并解决问题,提高系统的稳定性和可靠性,保障用户体验,确保业务的正常运行。
4.1 白屏分类
导致白屏的原因大致可以分为两大类:
-
JavaScript执行错误:这种情况通常是逻辑错误引起的,一般伴随着功能流程的阻断,难以通过等待或刷新等方法恢复,需要开发人员介入修复。比如,React组件发生了异常,并且外部没有错误边界捕获错误,那么React组件render挂载目标节点下的DOM树就会被清空,出现白屏现象。
-
请求未返回:可以细分为可恢复和不可恢复两种情况
-
可恢复:第一次进入页面时,由于资源加载过慢或接口请求未返回,经过等待和刷新之后就能恢复。这种情况一般是由于网络状况或者设备性能太差等原因导致的,可以通过监控首屏时间来发现,如果生产环境的首屏时间出现异常上升趋势,开发人员应该及时关注并排查最近改动的代码
-
不可恢复:可能是CDN服务器异常、域名劫持等原因导致的,可以通过资源保障来建立防御性措施。
-
经过以上分析,请求未返回导致的白屏可以通过性能监控和资源保障来进行防御,开发人员需要重点关注的是JavaScript执行错误导致的白屏问题。
4.2 异常白屏监控
目前,开源社区的大多数主流前端框架会将DOM挂载到一个目标根节点下,当内部代码发生JavaScript错误并且未做处理时,框架会将根节点下的所有DOM卸载,从而导致页面出现白屏。
针对这种情况,我们可以在异常监控中增加白屏的判断处理,具体做法就是在全局捕获JavaScript错误时对可视区域的DOM节点进行检测,判断是否存在可见的内容,有两点值得注意:
-
提升检测节点的效率:为了减少遍历节点数量,可以使用深度优先算法进行优化,检测到某一层的节点存在可见内容时,就不需要对其后代节点进行检测了。
-
判断节点是否有可见内容:先过滤掉非视图标签的节点,比如
meta
、link
等;然后根据节点的尺寸以及display、opacity、visibility等CSS属性来判断节点是否存在可见内容。
const whiteScreenObserve = (dom) => {
// 检测白屏
function checkForWhiteScreen(error) {
const hasVisibleContent = checkNodeVisible(dom);
if (!hasVisibleContent) {
console.log('白屏异常发生');
reportWhiteScreen({
pageUrl: window.location.href,
error
})
}
}
window.addEventListener('unhandledrejection', checkForWhiteScreen);
window.addEventListener('error', checkForWhiteScreen);
return () => {
window.removeEventListener('unhandledrejection', checkForWhiteScreen);
window.removeEventListener('error', checkForWhiteScreen);
}
}
function checkNodeVisible(node) {
// 已卸载的节点不可见
if (!document.body.contains(node)) {
return false;
}
const children = node.childNodes;
for (let i = 0; i < children.length; i++) {
const element = children[i];
// 过滤掉非视图标签
if (!isNonViewTag(element.tagName)) {
// 如果当前节点不可见,则后代节点也不可见
if (!isVisibleElement(element)) {
continue;
}
const { width, height } = getElementSize(element);
// 如果当前节点可见,即代表可见
// 如果当前节点不可见,则需要继续判断后代节点是否可见;
if ((width > 0 && height > 0) || checkNodeVisible(element)) {
return true;
}
}
}
return false;
}
function isNonViewTag(node) {
const nonViewTags = ['META', 'LINK', 'SCRIPT', 'STYLE', 'HEAD', 'TITLE'];
return nonViewTags.includes(node.tagName);
}
function isVisibleElement(node) {
const style = window.getComputedStyle(node);
// 检查元素是否可见
if (style.display!=='none' && style.visibility!=='hidden' && +style.opacity !== 0) {
return true;
}
return false;
}
function getElementSize(node) {
const style = window.getComputedStyle(node);
return {
width: +style.width,
height: +style.height
}
}
以上是一个简单的实现。在实际场景中,还需要根据实际情况自定义白屏的判断条件。
4.3 实时白屏监控
上面的异常白屏监控只能监控到 JavaScript 异常时的白屏场景,无法覆盖其他白屏场景,比如由于耗时任务引起性能问题、网络状况不佳、设备性能过低等原因导致的白屏。对于这类白屏场景,就需要在DOM变更时进行实时白屏监控。
浏览器提供了 MutationEvent
和 MutationObserver
两种方法,可以监听DOM树的变化情况。
MutationEvent
:当 DOM 树的结构发生改变时同步触发,每次变动都会触发一次调用,这可能会导致性能问题,所以在现代开发中一般不推荐使用。MutationObserver
: 是一种更现代、性能更优的监听 DOM 树变化的机制。它使用异步回调的方式,首先创建一个MutationObserver
对象,并通过observe
方法指定要观察的目标元素和观察的配置选项。当观察的 DOM 变化时,它会将这些变化记录下来,然后在主线程执行完毕后,以异步的方式调用指定的回调函数,批量处理变更。它的性能更好,是目前推荐用于监听 DOM 变化的方法。
因此,我们可以使用 MutationObserver
在DOM变更时进行白屏检测。
// 创建 MutationObserver 实例
let observer = new MutationObserver(function (mutationsList, observer) {
checkForWhiteScreen(mutationsList);
});
// 观察目标节点,检测白屏
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 定义白屏检查函数
function checkForWhiteScreen(mutationsList) {
let shouldCheck = false;
for(const mutation of mutationsList) {
// 节点属性变更、节点移除时需要检测白屏
if (mutation.type === 'attributes' || mutation.removedNodes) {
shouldCheck = true;
break;
}
}
const isWhiteScreen = checkNodeVisible(targetNode);
if (isWhiteScreen) {
console.log('白屏异常发生');
reportWhiteScreen();
}
}
以上是实时白屏监控的一个简单的实现。另外,对于请求未返回的白屏场景,还需要通过性能监控、资源保障等手段来优化。
5. 卡顿监控
页面流畅度关乎用户体验的好坏,对于开发人员而言,页面的卡顿监控是非常重要的环节。
5.1 卡顿分析
为了分析页面的卡顿情况,就需要对卡顿有一个明确的客观的定义。
FPS
是图像领域的概念,指动画或视频每秒传输的帧数,每秒传输的帧数越多,显示的动作就越流畅,当FPS太低时,就会出现肉眼可见的卡顿。因此,FPS 可以作为衡量页面流畅度的指标。通常来说:
- FPS 达到 50 ~ 60:动画将会相当流畅
- FPS 低于 30:动画出现明显的卡顿
Chrome 浏览器的开发者工具提供了 FPS 功能,可以在命令菜单中搜索关键字“FPS”打开Frame Rendering Status
功能,此时页面上就会出现一个悬浮窗,展示与页面渲染相关的FPS以及GPU的使用情况。
5.2 模拟FPS
由于浏览器渲染引擎实现机制、硬件和环境的差异,以及性能开销和动态环境因素等种种原因,目前没有标准的 Web API 可以直接获取 FPS。
开发人员可以使用现有的API来间接计算和监测 FPS,这样可以灵活地根据具体需求调整测量方式和频率,满足大多数情况下的性能分析和优化需求。
PerformanceObserver
可以使用 PerformanceObserver
API监听 frame
条目来模拟计算FPS:
const observer = new PerformanceObserver(list => {
for (let entry of list.getEntries()) {
// 计算每帧之间的时间差可以得到每帧渲染的时长
// 统计1s之内的帧数可以得到FPS
}
});
observer.observe({ entryTypes: ['frame'] });
但是,使用PerformanceObserver
API计算FPS存在一些问题:
- 浏览器兼容性:不同的浏览器对
PerformanceObserver
的frame
条目类型的支持可能不一致。有些浏览器可能不会报告frame
条目,导致无法准确计算fps
或根本无法计算。 - 性能条目频率:不是每个帧都会产生性能条目,这可能导致
fps
的计算不够连续或精确。 - 性能开销:虽然
PerformanceObserver
是异步的,但频繁的性能观察和计算可能会带来一定的性能开销,需要谨慎使用该方法,避免性能观察对页面性能产生过大的影响。
requestAnimationFrame
浏览器提供了requestAnimationFrame
API,它接受一个回调函数作为入参,浏览器会在下次重绘之前调用回调函数。在大多数遵循W3C标准的浏览器中,回调函数的执行次数跟浏览器屏幕的刷新次数匹配。通常来说,Web 浏览器的刷新频率是60Hz,则回调函数每秒执行60次,每16.7ms执行一次。因此,可以用这个API来模拟计算 FPS:
function calcFPS() {
// 存储上一次计算 FPS 的时间戳
let prevTime = performance.now();
// 记录在一秒内调用 frame 函数的次数
let frames = 0;
let fps;
function frame() {
let now = performance.now();
frames++;
if (now >= prevTime + 1000) {
fps = Math.round((frames * 1000) / (now - prevTime));
console.log('FPS:', fps)
prevTime = now;
frames = 0;
}
window.requestAnimationFrame(frame);
}
window.requestAnimationFrame(frame);
return fps;
}
如果requestAnimationFrame
API不支持,那么可以用setTimeout
API来降级模拟。为了保障页面的流畅性,FPS的数值不能低于60,因此setTimeout
的时间间隔可以设置为(1000 / 60)
,也就是将1s切分成60份,每隔16ms创建一个定时任务,计算1s内定时任务执行的次数,以此来计算FPS。
使用 requestAnimationFrame
API 来模拟计算 FPS 是目前比较推荐的方案。有以下几个原因:
- 浏览器兼容性好:它是一个稳定且标准的浏览器 API,几乎所有现代浏览器都支持它。
- 精确性和实时性高:它是专门为动画和高性能渲染而设计的 API,与浏览器的渲染周期紧密同步,可以更精确地反映出页面的实际渲染频率。
- 性能开销优化:与浏览器的渲染周期同步,只会在需要更新动画或进行渲染时调用。此外,当页面进入后台运行或处于隐藏的iframe中,
requestAnimationFrame
通常会被暂停调用,从而提高性能和寿命。
6. 用户行为监控
用户行为监控是指对用户在页面上进行的各种操作进行采集,记录用户每次会话过程的操作路径,从而还原用户的使用场景、操作规律、访问路径和行为特点。它能带来诸多收益:
- 从产品角度:提供用户真实的操作路径和偏好数据,有助于验证产品的可行性,精准规划产品功能迭代方向,打造更贴合用户需求的产品。
- 从设计角度:可以展现用户与页面元素的交互情况,从而优化页面布局和交互设计,提升视觉吸引力和易用性。
- 从运营角度:可以了解用户的活跃程度、内容偏好等,全面挖掘用户的使用场景,以此制定更精准的营销策略,如推送个性化内容、活动,有效提高用户参与度和留存率。
- 从研发角度:可以帮助确定错误的操作路径、精准定位问题根因,从而快速修复问题,保障系统的稳定性和性能优化。
下面介绍两种与操作路径相关的监控方法:一种是通过事件监听收集关键用户行为;另一种是通过页面录制,然后通过视频回放的形式对用户操作进行复现。
6.1 事件监听
事件监听主要用于收集用户的关键行为。操作事件大致可以分为:
- 鼠标事件:用户使用鼠标进行交互时触发的一系列事件,包括
click
、dblclick
、mousedown
、mouseup
、mousemove
和mouseover
、mouseout
等。除了记录鼠标事件,还需要记录触发事件的 DOM 节点信息。 - 键盘事件:用户操作键盘时触发的事件,主要包括
keydown
、keyup
和keypress
等。还需要记录键盘的编码等信息。 - AJAX 请求:包括请求发起和取消,可以通过劫持
XHR
、Fetch
的原生方法进行采集。采集的信息可以包括请求的地址、方法、参数和报头等,在实际应用中可以根据需要自定义采集的信息。比如,如果后端服务提供了根据UUID查询请求详情的功能,那么只需要采集请求的地址、方法和UUID即可。 - URL 变更:通过popstate监听浏览器页面地址的变化,记录前后的URL信息。
除了记录行为的自定义属性,还需要收集一些通用属性,例如时间戳、页面URL、会话ID、用户ID等。操作路径的信息会记录在内存中,当页面销毁时自动释放,因此需要定期上传并清空本地的actions记录。
常用的工具有 Google Analytics、Mixpanel、GrowingIO 和 Sentry 等。
6.2 录播回放
通过对页面进行录制和回放可以更精准、更直观地对用户操作行为进行记录和复现。
录制的信息通常包括:
- 操作事件监听:通过事件监听记录用户的鼠标、键盘等操作事件
- DOM 变化记录:通过使用浏览器提供的
MutationObserver
API来观察页面的文档对象模型(DOM)的变化情况 - 页面状态和时间戳记录:记录每个时间点上页面的状态信息,如当前的 URL、页面滚动位置等;时间戳可以使用浏览器的高精度时间源(如
performance.now()
),这样在回放时可以按照正确的时间顺序重现用户的操作。
这些记录信息会使用JSON格式记录并序列化,然后进行本地存储和上传。当需要远程回放时,相关的记录信息会被提取,然后进行数据解析与反序列化,根据记录的时间戳,按照顺序模拟之前记录的各种事件。同时,通过记录的 DOM 变化信息,在回放环境中逐步重建页面的 DOM 状态。
常用的录播回放工具有:rrweb、FullStory、LogRocket 和 SessionStack等。