本文是如何监控网页的卡顿? 的下篇. 今天我们把话题聚焦在如何监控网页的崩溃上.
寸志: 如何监控网页的卡顿?zhuanlan.zhihu.com
崩溃和卡顿有何差别?
卡顿也就是网页暂时响应比较慢, JS 可能无法及时执行, 这也是上篇网页卡顿监控所依赖的技术点.
但崩溃就不一样了, 网页都崩溃了, 页面看不见了, JS 都不运行了, 还有什么办法可以监控网页的崩溃, 并将网页崩溃上报呢?
但, 天无绝人之路, 方法总是有的.
load 与 beforeunload 事件
搜遍互联网, 几乎找不到方法, 最终碰上了这篇文章. 本文利用 Windows 对象的 load 和 beforeunload 事件实现了网页崩溃的监控.
- http://jasonjl.me/blog/2015/06/21/taking-action-on-browser-crashes/jasonjl.me
- Windows.addEventListener('load', function () {
- sessionStorage.setItem('good_exit', 'pending');
- setInterval(function () {
- sessionStorage.setItem('time_before_crash', new Date().toString());
- }, 1000);
- });
- Windows.addEventListener('beforeunload', function () {
- sessionStorage.setItem('good_exit', 'true');
- });
- 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'));
- }
一图胜千言:
使用 load 和 beforeunload 事件实现崩溃监控
这个方案巧妙的利用了页面崩溃无法触发 beforeunload 事件来实现的.
在页面加载时 (load 事件) 在 sessionStorage 记录 good_exit 状态为 pending, 如果用户正常退出 (beforeunload 事件) 状态改为 true, 如果 crash 了, 状态依然为 pending, 在用户第 2 次访问网页的时候(第 2 个 load 事件), 查看 good_exit 的状态, 如果仍然是 pending 就是可以断定上次访问网页崩溃了!
但这个方案有问题:
采用 sessionStorage 存储状态, 但通常网页崩溃 / 卡死后, 用户会强制关闭网页或者索性重新打开浏览器, sessionStorage 存储但状态将不复存在;
如果将状态存储在 localStorage 甚至 Cookie 中, 如果用户先后打开多个网页, 但不关闭, good_exit 存储的一直都是 pending, 完了, 每有一次网页打开, 就会有一个 crash 上报.
全民直播 https://www.quanmin.tv/ 一开始采用的就是这个方案, 发现就算页面做了优化, crash 不下降, 与 PV 保持比例, 才意识到这个方案的问题之处.
基于 Service Worker 的崩溃统计方案
随着 PWA 概念的流行, 大家对 Service Worker 也逐渐熟悉起来. 基于以下原因, 我们可以使用 Service Worker 来实现网页崩溃的监控:
Service Worker 有自己独立的工作线程, 与网页区分开, 网页崩溃了, Service Worker 一般情况下不会崩溃;
Service Worker 生命周期一般要比网页还要长, 可以用来监控网页的状态;
网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息.
基于以上几点, 我们可以实现一种基于心跳检测的监控方案:
p1: 网页加载后, 通过 postMessage API 每 5s 给 sw 发送一个心跳, 表示自己的在线, sw 将在线的网页登记下来, 更新登记时间;
p2: 网页在 beforeunload 时, 通过 postMessage API 告知自己已经正常关闭, sw 将登记的网页清除;
p3: 如果网页在运行的过程中 crash 了, sw 中的 running 状态将不会被清除, 更新时间停留在奔溃前的最后一次心跳;
sw:Service Worker 每 10s 查看一遍登记中的网页, 发现登记时间已经超出了一定时间 (比如 15s) 即可判定该网页 crash 了.
一些简化后的检测代码, 给大家作为参考:
- // 页面 JavaScript 代码
- if (navigator.serviceWorker.controller !== null) {
- let HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒发一次心跳
- let sessionId = uuid();
- let heartbeat = function () {
- navigator.serviceWorker.controller.postMessage({
- type: 'heartbeat',
- id: sessionId,
- data: {} // 附加信息, 如果页面 crash, 上报的附加数据
- });
- }
- Windows.addEventListener("beforeunload", function() {
- navigator.serviceWorker.controller.postMessage({
- type: 'unload',
- id: sessionId
- });
- });
- setInterval(heartbeat, HEARTBEAT_INTERVAL);
- heartbeat();
- }
sessionId 本次页面会话的唯一 id;
postMessage 附带一些信息, 用于上报 crash 需要的数据, 比如当前页面的地址等等.
- const CHECK_CRASH_INTERVAL = 10 * 1000; // 每 10s 检查一次
- const CRASH_THRESHOLD = 15 * 1000; // 15s 超过 15s 没有心跳则认为已经 crash
- const pages = {}
- let timer
- function checkCrash() {
- const now = Date.now()
- for (var id in pages) {
- let page = pages[id]
- if ((now - page.t)> CRASH_THRESHOLD) {
- // 上报 crash
- delete pages[id]
- }
- }
- if (Object.keys(pages).length == 0) {
- clearInterval(timer)
- timer = null
- }
- }
- worker.addEventListener('message', (e) => {
- const data = e.data;
- if (data.type === 'heartbeat') {
- pages[data.id] = {
- t: Date.now()
- }
- if (!timer) {
- timer = setInterval(function () {
- checkCrash()
- }, CHECK_CRASH_INTERVAL)
- }
- } else if (data.type === 'unload') {
- delete pages[data.id]
- }
- })
都挺简单的代码, 不细说了.
方案的可行性
兼容性:
Service Worker 的普及率已经相当高了 https://caniuse.com/#search=service worker , 鉴于国内各种浏览器都是 Chrome 内核, 而且版本已经在 Chrome 45 以上, 已经覆盖了相当一部分用户. 作为监控, 数据覆盖大部分就好.
Service Worker 兼容性
可靠性:
这应该是我目前已知可以相对准确判断出网页崩溃的方式了. 不过我们的方案还在测试环境, 上线一段时间后再给大家共享数据.
对浏览器厂商的建议
题图的 Crash 列表, 可以在 Chrome 中访问 Chrome://crashes/ 看到, 如果厂商可以提供一个 API, 在页面打开时, 可以获知用户上一次崩溃的信息就很棒了!
来源: https://juejin.im/entry/5be158116fb9a049c6434f4a