前端页面崩溃监控全攻略:心跳判定 + Service Worker 接管

背景

在浏览器环境中,"页面崩溃(Page Crash)"并不是一个浏览器主动抛出的可监听事件。 页面可能因为以下原因被动终止:

  1. 内存 OOM 导致 tab 崩溃

  2. 浏览器内部的 renderer 进程挂掉

  3. 业务代码死循环、长任务阻塞导致页面卡死

  4. 页面在后台被系统杀进程(尤其移动端)

  5. 浏览器关闭 / 标签页关闭但执行不到 beforeunload(常见)

由于浏览器没有提供"页面是否异常退出"的 API,因此前端监控体系通常只能间接推断崩溃

本次调研希望解决以下两个问题:

🎯 目标

  1. 如何在单标签页场景中准确推断页面是否异常退出?

  2. 多标签页环境中,一个页面崩溃后,如何被其他页面检测并上报?

  3. 是否能做到不依赖页面再次打开 ------ 即实时上报?(如 Service Worker)

  4. 最终方案应尽量稳定、低侵入、可扩展并减少误报率

调研方案

1. 基于「退出打标 + 心跳检测」的崩溃推断方案

这是目前业内最常见的思路,例如不少监控 SDK 都采用类似机制。

1.1 核心思路

  1. 页面正常退出时(beforeunload / pagehide / visibilitychange)写入 normalExit = true

  2. 如果是崩溃,则正常退出钩子不会触发 → normalExit 保持 false

  3. 下次启动页面时读取存储(localStorage),若发现上次 normalExit=false,则认为存在异常退出

单页面流程示意图

arduino 复制代码
正常退出 → normalExit = true → 下次打开不告警
崩溃 → normalExit = false 且心跳断更 → 下次打开上报崩溃

// 单页面检测崩溃代码

js 复制代码
// 伪代码,还需要处理beforeunload / pagehide / visibilitychange
window.addEventListener('beforeunload', () => {
  localStorage.setItem('normalExit', 'true');
});
function checkCrash() {
  const normalExit = localStorage.getItem('normalExit');
  if (normalExit !== 'true') {
    reportCrash();
  }
  localStorage.setItem('normalExit', 'false');
}

上面只是讨论到当页面单开的情况,那么如果是多标签页的场景下该如何设计呢?要知道localStorage是在同一个域名下各个标签页共享的。

根据上面的检测原理,会想到,给每个页面都设置一个独立的 tabId,并在其中一个页面获取所有的页面normalExit,判断normalExit 是不是 false,false则认为是发生了崩溃。

但是不行,当我们打开多页面时,因为页面并还没有退出,获取到的normalExit其实都是false的,那我们就需要多一个字段去判断,心跳时间;

1.2 多标签页场景的完整设计

针对多页面场景,每个页面需要心跳 + tabId,判断页面是否还存活

为什么需要心跳?

因为多页面时,每个页面运行中时 normalExit 本来就是 false。 所以不能只看 normalExit,需要结合"最后心跳时间":

ini 复制代码
normalExit = false + 心跳超过阈值未更新 → 判定崩溃

👇 关键逻辑代码(精简示例)

** 为每个 tab 创建唯一 ID **

js 复制代码
// 
function getTabId() {
  let id = sessionStorage.getItem('**tab_id**');
  if (!id) {
    id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
    sessionStorage.setItem('**tab_id**', id);
  }
  return id;
}

心跳写入(运行中 normalExit = false)

ts 复制代码
function saveAll(map: Record<string, HeartbeatRecord>) {
  try {
    localStorage.setItem(KEY, JSON.stringify(map));
  } catch {}
}

export function writeHeartbeat(rec: Omit<HeartbeatRecord, 'ts' | 'tabId'>) {
  const tabId = getTabId();
  const map = loadAll();
  map[tabId] = { ...map[tabId], ...rec, ts: Date.now(), tabId };
  saveAll(map);
}

export function startSessionHeartbeat(intervalMs = 3000, recBase?: Omit<HeartbeatRecord, 'ts' | 'tabId'>) {
  const page = `${window.location.pathname}${window.location.hash || ''}`;
  let timer: number | undefined;

  function beat() {
    writeHeartbeat({ page, version: recBase?.version, env: recBase?.env, meta: recBase?.meta, normalExit: false });
  }

  beat();
  timer = setInterval(beat, intervalMs);

  window.addEventListener('beforeunload', () => {
    markNormalExit();
    if (timer) clearInterval(timer);
    clearTabHeartbeat();
  });

  return () => {
    if (timer) clearInterval(timer);
  };
}

正常退出钩子(beforeunload)

js 复制代码
export function markNormalExit() {
  const tabId = getTabId();
  const map = loadAll();
  if (map[tabId]) {
    map[tabId].normalExit = true;
    saveAll(map);
  }
}

window.addEventListener('beforeunload', () => {
  markNormalExit(); // 标记正常退出
  clearTabHeartbeat(); // 清除心跳
});

// 重新打开页面时判断历史页面是否崩溃
if (!rec.normalExit && diff > timeoutMs) {
  reportCrash(rec);
}

下次打开时检测崩溃(核心)

判断崩溃逻辑:

  1. 不是正常退出时,normalExit 为 false

  2. 当崩溃时,normalExit 为false 或 undefined

  3. diff > timeoutMs 主要是为了当多开页面时,正常的页面心跳时间一直在滚动更新,不会少于timeoutMs,防止误报

ts 复制代码
export function checkPreviousAbnormalExit(
  timeoutMs: number,
  report: (payload) => void
) {
  const now = Date.now();
  const currentTab = getTabId();
  const map = loadAll();

  let changed = false;
  Object.values(map).forEach((rec) => {
    if (!rec || rec.tabId === currentTab) return;
    const diff = now - (rec.ts || 0);

    if (!rec.normalExit && diff > timeoutMs) {
      //// diff > timeoutMs 有个弊端,当用户崩溃后,刚好设了一个时间戳,并且马上打开一个新标签页,这时diff可能还没超过timeoutMs,这种情况会漏报
      report({ ...rec, diff });
      delete map[rec.tabId];
      changed = true;
    } else if (rec.normalExit) {
      delete map[rec.tabId];
      changed = true;
    }
  });

  if (changed) saveAll(map);
}

1.3 多页面心跳的隐藏坑点(必须处理)

🔥 1. 页面隐藏时定时器会被延迟

浏览器切后台后,setInterval 会被降频,甚至几秒才执行一次。 这会误伤心跳逻辑。

处理方式:

页面隐藏时直接标记 normalExit=true,避免误报。

但为什么可行?因为后台页面本身不应计入崩溃统计(用户没在看)。

🔥 2. 心跳的时间不准

如果是使用定时器更新心跳时间,心跳更新时间并不会特别准可以了解下setInterval原理,比如设了3000毫秒更新一次,有可能是3000+-N000毫秒才执行更新,也可能是主线程有大计算导致更新时间更慢,所以阈值不能设置和心跳时间一样的时间,得有一定的宽容度。 建议阈值为:

复制代码
阈值 = 心跳间隔 \* 2

1.4 方案一的优劣总结

优点

  • 实现简单,不依赖 Service Worker

  • 能检测页面是否在上一次会话中异常退出

  • 多标签页可准确判断单页崩溃

缺点(重点)

  • 无法实时上报,必须等待下次打开页面

  • 页面进程在后台被系统杀死,无法触发页面生命周期事件导致无法打标记,下次启动页面时会存在误报。但是误报笔者是觉得允许的,采集到样本大的页面崩溃路径才是最有可能导致崩溃的页面;

2. 基于 Service Worker 的实时心跳监控

既然方案一无法实时上报,那么是否能借助Worker 实现实时上报呢?worker 独立于页面运行,可以在页面崩溃后继续存活,从而实现实时上报。

笔者一开始是想到用 Web Worker 来实现的,但是后来发现 Web Worker 生命周期和页面是绑定的,页面崩溃后,Web Worker 也不可用,所以无法实现实时上报。只能是用service Worker 来实现。

Service Worker(SW)可以在页面崩溃后继续存活,只要浏览器进程未关闭。 利用 SW 作为"监控总控",页面与 SW 双向通信,从而实现:

✔ 页面实时心跳发送

✔ SW 主动判断某 tab 心跳超时

✔ 立即上报崩溃事件(无需等待下次进入)

2.1 方案架构图

css 复制代码
页面 A/B/C
  ↓ (heartbeat)
Service Worker(独立线程)
  ↓ (report)
监控服务(如 Sentry)

2.2 SW 的核心逻辑

  1. 收到心跳,更新 tab 的 时间搓
js 复制代码
if (data.type === 'heartbeat') {
  tabLastBeat.set(tabId, { ts: now() });
  ensureCrashChecker();
}
  1. 定时检查哪些 tab 心跳超时
js 复制代码
if (nowTs - ts > CRASH_TIMEOUT_MS) {
sendReport();
tabLastBeat.delete(tabId);
}
  1. 页面向 SW 发送 exit 消息,避免误报
js 复制代码
postToSW({ type: 'exit', tabId: this.tabId });

2.3 页面侧的心跳通知

js 复制代码
this.timer = setInterval(() => {
  this.postToSW({
    type: 'heartbeat',
    tabId: this.tabId,
    ts: Date.now()
  });
}, this.heartbeatIntervalMs);

同时绑定生命周期:

  • beforeunload

  • visibilitychange(hidden → exit)

  • pagehide

2.4 方案二的优劣

优点

  • 支持实时上报

  • 监控逻辑不依赖浏览器是否回到页面

  • 多标签页信息共享更自然(SW 本来就是共享运行时)

缺点(非常关键)

  • 浏览器窗口关闭时,SW 也会消失 → 无法上报

  • 仅适用于开启 SW 的站点(需 HTTPS + 同源)

  • SW 更新策略复杂(需处理 skipWaiting、claim 等)

最终结论与推荐方案

场景 最佳方案 原因
简单不复杂 方案一(localStorage + 心跳) 下次打开可判断所有异常退出,覆盖范围最大
想实时上报崩溃(非浏览器关闭情况) 方案二(Service Worker) 页面崩溃后 SW 仍可运行并上报
希望误报率最低 结合两者 SW 实时 & 下次打开兜底

附录

  1. 完整方案一代码示例见:页面崩溃上报实现代码
  2. 方案二 Service Worker 示例代码见:Service Worker 页面崩溃监控代码Service Worker 代码
  3. 完整demo 见:页面崩溃监控 Demo

如果觉得代码有用麻烦点个小星星。

相关推荐
小蝙蝠侠3 小时前
async-profiler 火焰图宽度是否可信?哪些情况下会误导?(深度解析)
java·性能优化
编织幻境的妖4 小时前
Python代码性能优化工具与方法
开发语言·python·性能优化
ManageEngine卓豪7 小时前
企业网站监控与性能优化指南
数据库·microsoft·性能优化
白帽子黑客杰哥8 小时前
WAF在云原生环境下的部署方案与性能优化策略
云原生·性能优化
赵财猫._.9 小时前
【Flutter x 鸿蒙】第七篇:性能优化与调试技巧
flutter·性能优化·harmonyos
kdniao19 小时前
iOS应用集成物流API接口:架构设计、性能优化与用户体验实践指南
ios·性能优化·ux
木易士心10 小时前
Vue 3 内存泄漏排查与性能优化:从入门到精通的工具指南
性能优化
LYFlied12 小时前
前端开发者需要掌握的编译原理相关知识及优化点
前端·javascript·webpack·性能优化·编译原理·babel·打包编译
workflower12 小时前
软件工程练习题COMET
性能优化·团队开发·需求分析·个人开发·scrum·敏捷流程·结对编程