1. 为什么我会自己动手写 rea 这个工具1.1 一次线上事故把我逼到了墙角事情是这样的。半年前我做的一个前端项目在发版后的第三天线上陆续出现首页白屏、按钮点了没反应、接口偶发超时报错的反馈。奇怪的是我在本地开发环境里怎么折腾都复现不出来切换网络节流、清缓存、换浏览器一切正常。当时团队里又没有部署完善的可观测体系我只能在线上环境打开浏览器控制台手动刷新几十次去碰运气。那个下午我刷了四十多遍页面终于看到一条Uncaught TypeError: Cannot read properties of undefined的报错但报错堆栈指向的是一个压缩混淆后的文件几乎没法定位。更痛苦的是这种偶发问题在用户那边出现过好几次每次都是刷新之后又好了用户根本不愿意配合慢慢排查。那一刻我非常清楚地意识到一件事缺少一个能常驻在真实环境里、记录关键过程数据、事后可回放的轻量工具。大多数情况我们只关注请求有没有失败但真正有用的信息是这个请求到底慢在哪一步资源加载顺序有没有问题JS 执行是不是阻塞了渲染。所以后面我专门花了两周多时间自己写了一个内部代号叫rea的小工具。它的全称我也没纠结太多项目里就写的是 Real Environment Assistant天天看板子上都挂着这个代号。核心目标就三个在真实环境下采集页面运行过程数据、把数据整理成看得懂的记录、在问题发生时能快速导出分析。这篇文章就把 rea 从设计到落地再到踩坑的完整过程整理出来希望给那些同样没有重可观测平台、又经常被线上诡异问题折磨的小团队一点参考。1.2 团队现有的工具覆盖不到的盲区可能有人会说这年头监控工具不是一抓一大把吗市面上开源的、商业的从 APM 到前端监控都有为什么还要自己写一个。我承认这些方案成熟、功能全面但在真实接入过程中总会碰到几种尴尬情况。第一种是结果指标多过程数据少。多数监控平台能告诉你某页面 JS 错误率 3.2%接口成功率 97%但当你想知道某个接口在弱网状态下 DNS 查询花了多少毫秒首屏图片加载是不是比脚本优先用户操作后主线程阻塞了多久这些细节拿不到。第二种是接入成本高要装 SDK、配 token、建账号、设告警规则对于只有两三个前端、后端可能还是兼职的小团队来说运营成本明显偏高。第三种更关键很多监控工具本身也会影响页面性能包体积大、上报频繁反而给生产环境又添了一层压力。 所以不同工具有不同的视野盲区。我之前用过一套按错误率、崩溃率、耗时中位数来告警的方案事情发生之后确实能收到通知但对于定位问题帮助有限。而 rea 的思路是反过来的不追求大而全只做过程记录的数字化捕捉器把关键行为的时间戳、顺序、耗时、状态码、资源大小、堆栈片段都留下来问题没发生的时候它安静蹲着问题发生了之后资料都在。1.3 rea 的定位与边界在动手写之前我先给自己定了几条规矩避免做着做着变成一个四不像项目rea 不是一个全链路 APM不做分布式追踪、不采集后端链路只管浏览器端到接口返回这个区间。rea不做自动告警它更像一个行车记录仪等你主动查看。rea优先本地存储数据默认存在浏览器里需要的时候再一键导出不强制上报到服务器。rea必须保持轻量整个压缩后体积不打算超过 15KB依赖必须是零用原生浏览器 API 完成所有采集。把这些边界写清楚之后开发方向就明朗了。rea 适合的场景是中小团队的前端项目、没有完整监控体系、排查线上偶发问题时需要关键现场数据。它也可以作为现有监控体系的补充层在出问题时能下钻到更细的过程级别。2. rea 的设计骨架三层管道加一个全局缓冲2.1 采集层能拿到的数据先拿到rea 的第一层是采集层任务很简单也很朴素把浏览器运行时能暴露的数据尽量采集下来。我选用了几个原生 API 作为数据来源这一层不需要任何库性能也足够好。对于资源加载我用PerformanceObserver监听resource类型的条目能拿到每个 JS、CSS、图片、字体、XHR/fetch 请求的名称、耗时、传输大小、协议版本、域名明细。它的优势在于能结合浏览器内部的性能时间线比我们手动包一层计时更精准。对于网络请求我包装了window.fetch与XMLHttpRequest在请求发起时记录时间戳、URL、请求方式在响应返回时记录状态码、耗时、响应体大致大小。为什么不只靠 resource timing因为它拿不到业务状态码也无法把接口和业务参数对应起来所以必须在上层再做一层包装。对于页面交互体验我用PerformanceObserver监听longtask把超过 50ms 的长期任务记录下来同时配合MutationObserver观察页面 DOM 变化造成的耗时。这一块主要是为了排查点了没反应这类交互卡顿问题。另外所有事件的监听都会带上一个会话 ID 和自增序列号方便后续把不同来源的数据在时间轴上对齐。2.2 加工层把原始数据变成能看懂的记录原始数据采集回来之后绝大部分是零散的、包含冗余信息的。比如一条资源记录里可能会有十几个字段但真正重要的是name、duration、decodedBodySize、initiatorType一条 longtask 记录也需要把它的开始时间换算成相对页面加载完成的偏移量。rea 的加工层做了几件事。第一是归一化把所有时间值统一成毫秒并把 URL 上的查询参数按规则裁剪避免同一接口因为带了个随机时间戳参数而被当成两条记录。第二是关联用自增序列号和会话 ID把一次操作触发的事件串联起来。第三是优先级排序把各类型记录按时间合并成一个统一的线性日志流这样在看报告时能清晰知道用户点了按钮 - 50ms 后发起接口请求 - 300ms 后接口返回 - 400ms 后执行了一个 longtask。加工层的设计看起来简单但非常关键。没有这一层你拿到的就是一堆割裂的表格有了这一层才能还原出问题发生前后的完整场景。这也是 rea 与直接打印 console 日志最本质的区别。2.3 缓冲层与输出层环形队列与一键导出rea 默认把数据存在一个全局缓冲队列里挂在window.__REA_BUFFER__上使用一个环形数组默认容量 2000 条记录超过之后自动丢弃最旧的数据。这样做的好处是内存占用是固定的不会因为页面长时间运行而无限膨胀。需要注意的是我没有一开始就把数据写入localStorage因为持久化的成本比较高而且在真实环境里可能每秒产生几十条记录连续写入会引发性能问题。rea 的策略是内存缓冲为主定期快照为辅。默认每 30 秒或者缓冲满时会压缩一次数据并写入到localStorage的rea-storage键里容量上限约 3MB超出时优先淘汰旧数据。同时 rea 暴露一个上报回调接口如果你在项目里配置了自定义上报也可以把数据直接推送到自己的服务端。在数据展示方面rea 会在控制台输出一个格式化面板通过一个特殊的前缀展示最近 N 条关键事件列表同时在页面上提供一个悬浮入口点开就是导出报告按钮会把缓冲数据导出成 JSON 文件。这样在出问题时直接把这个 JSON 发回给开发同学用 rea 自带的解析器一打开就能还原整个现场。// rea 控制台面板的简化调用示例 window.__REA__.exportReport().then((blob) { // blob 就是完整的 JSON 报告文件 console.log([rea] 报告已生成可下载后交给开发人员分析); });3. 接入 rea 的实操过程与配置细节3.1 部署方式复制一个 JS 文件还是走构建插件rea 在设计初期就决定不给使用者增加构建链复杂度。它提供了两种接入方式你完全可以按项目情况选择。第一种是脚本直插直接把编译压缩好的rea.min.js复制到项目静态目录然后在 HTML 的head里用script src/rea.min.js async引入。这种方式的好处是业务代码零侵入不加任何打包配置但缺点是不能自动使用业务项目的环境变量。第二种是构建插件注入我用一个简单的 Webpack/Vite 插件把 rea 的初始化代码注入到入口 chunk 里。这样可以在构建时读取环境变量来预设采样率、服务地址、白名单等配置。对于用 NPM 管理依赖的工程化项目这种方式更干净一些。我的建议是本地开发和测试环境可以使用脚本直插做验证正式项目建议走构建插件注入因为它能保证你拿到的是与业务构建产物一致的配置。如果你觉得引入额外插件很重也可以直接在项目入口的第一行调用初始化方法只要保证它比任何业务逻辑都先执行即可。3.2 配置文件字段与常见取值rea 的初始化配置非常简单下面是一个 Vite 环境下通过构建插件注入的完整示例{ enabled: true, sampleRate: 3, ignorePaths: [/api/health, /api/heartbeat], ignoreDomains: [hm.baidu.com, cnzz.com], maxBufferSize: 2000, storageKey: rea-storage, storageQuota: 3072000, exposeGlobal: true, onReport: null }这里面几个字段值得说明一下。sampleRate是采样率默认 3表示一次会话最多记录 3MB 原始数据不是按百分比采样。ignorePaths和ignoreDomains是一个黑名单用于过滤掉健康检查、心跳检测和第三方统计脚本的请求避免这些噪音数据把真正的问题淹没掉。maxBufferSize决定了内存里的记录条数默认 2000 通常够用如果你的页面在极短时间内产生大量事件可以调大这参数但要注意内存开销。storageQuota是本地存储的上限单位是字节。这块我建议控制在 2-3MB 左右因为localStorage在多数浏览器下的总配额只有 5MB你还要给业务数据留空间。onReport是一个回调函数如果你想把数据上报到自己的服务器可以在这里设置一个异步发送函数rea 会定期以 chunk 的方式把压缩后的数据传给它。3.3 从 rea 报表到行动一次真实的瓶颈定位接入 rea 之后不久我就在模拟项目 X 上抓到过一次典型的性能问题。用户的反馈是这个页面上传文件时接口偶尔会卡在已暂停状态进度条长时间不动。我用 rea 导出的报告往回分析发现时间轴上有两条记录非常可疑记录编号 1172POST /api/upload/chunkstatus 200耗时 2310msTCP 连接建立耗时 822ms记录编号 1175resource 腾讯地图脚本耗时 1180ms加载期间有 300ms 的长任务阻塞脚本执行这两条记录单独看都不算致命但放在一起就解释了用户遇到的问题文件上传是分片上传每片之间要用 JS 处理数据并构建 FormData恰好在一片中需要加载并执行地图相关的第三方脚本这个脚本把 JS 主线程阻塞了 300ms导致上传请求的发起步被延后进度条自然就显示卡住。之后我的处理方式是给第三方地图脚本加了async加载属性并把它移到上传组件初始化完成之后再加载问题立刻缓解。这个案例让我更确信一点很多真实环境下的性能怪相不是单个请求慢而是多个事件撞在一起。没有一个按时间线排列的过程记录工具这种问题很难复现也更难验证修复效果。rea 的核心价值就是它能让你在事后用回放代替复现。4. 一个月使用下来踩过的坑4.1 懒加载图片引发的误报风暴第一次把 rea 装上线第二天我就发现报告里全都是某个轮播图接口的请求错误记录。正常来看应该是偶发的结果数据里显示每分钟都有一堆失败请求。我一开始还以为是后端接口出了问题后来仔细看报告才发现这些请求的initiatorType全是img而且请求的目标地址是/api/user/avatar?userId...。原因其实很简单页面头像图片在用户滚动到可视区域之前浏览器不会真正发起请求。我的采集逻辑监听的是资源加载事件当滚动事件触发大量懒加载图片时rea 把图片尚未加载误判成了请求报错。后来我在采集逻辑里增加了状态判断只把那些真正开始加载且加载失败的资源记录为错误而不是把未加载资源当作错误。这个问题的教训是处理采集数据时要区分没有发生和失败否则异常率会被虚假数据极度放大。调整之后误报比例直接从 40% 降到了 0.5% 以下。4.2 跨域资源 timing 数据缺失resource timing 虽然强大但它有一个硬限制如果你的页面请求了跨域资源且该资源没有返回Timing-Allow-Origin响应头那么除了name、duration、transferSize这些基本信息之外DNS、TCP、TLS、响应体大小等详细字段全部会被浏览器隐藏。这意味着你在分析第三方 CDN 脚本、字体文件、地图脚本时看到的耗时数据基本是黑盒状态。rea 之前完全依赖这些 timing 字段导致跨域资源慢的时候根本不知道慢在哪一段。后面我在采集层加入了 fallback 机制当某个字段缺失时用performance.now()在 fetch/XHR 包装层做一个粗略计时至少能看到发起请求到拿到响应的总耗时和响应状态。另外在报告里会给跨域资源打上一个受限数据的标签提醒查看者谨慎解读。4.3 多标签页共用本地存储导致的数据竞争rea 用localStorage存储快照时我踩了一个比较隐蔽的坑。用户在同一个站点下可以打开多个标签页每个标签页都会运行 rea也都尝试往同一个rea-storage键里写数据。结果就是后写的标签页会直接覆盖先写的标签页的数据而每个标签页都认为自己保存的是完整记录最终导出的报告会莫名缺少前半段内容。我修复的办法是改用BroadcastChannel来协调多标签页在同一时间只允许一个主标签页负责持久化其他标签页把数据通过 channel 传给主标签页统一写入。同时存储结构从单键改成多个分片键每个分片键带上标签页 ID 和写入时间戳读取时合并所有分片。这个问题在单页应用不常见但如果你的项目是那种用户可以开多个标签页操作的后台系统一定要考虑多标签页的存储隔离问题否则数据会互相覆盖。4.4 长任务误判与异步时序错乱longtask 的监听本身没问题但一开始我把长任务期间发生的所有事件都归因于某个用户操作时出现了不少假关联。原因在于 JS 的异步执行模型一个 async 函数里的代码会被拆成多个微任务中间穿插着宏任务。当一个长任务触发时真正导致该任务堆积的代码可能在前几百毫秒已经执行完了这时候采集到的事件会产生时序错乱。rea 的关联机制一开始是按时间就近匹配后来我改成按任务边界分组用一个任务栈记录当前同步执行上下文异步任务入队时先记录当时的栈信息事件发生时就找最近的栈帧归属。这样虽然实现上多了几十行代码但关联准确率显著提升尤其在排查点击按钮后 800ms 才发起请求这类问题时能清楚看到中间那 800ms 到底发生了什么而不只是看到一个孤零零的延迟。5. 关于 rea 的一些后续想法5.1 从观测工具迈向体检工具现阶段 rea 给我的感觉更像一个事后回放机。虽然它解决了我很大的问题但回放终归是被动的。我接下来的计划是在回放数据基础上增加体检报告能力每次会话结束或者定期离线聚合时自动根据数据生成一份类似体检单的概览里面直接列出性能隐患项。比如检测到某些资源持续超过 500ms 且还未设置cache-control就标记为缓存缺失如果某个接口的耗时中位数是 800ms但在慢速网络下跃升到 3000ms就会标记为弱网表现不佳再比如长任务出现频率超过某个阈值就标记为主线程负载过高。这实际上是在把开发者的排查经验变成规则集让工具从记录异常进化到提示隐患。目前实现这层需要在前端统计上积累一段时间的基线数据才能定出相对合理的阈值所以还在小范围试跑。5.2 我建议的接入范围与实际体会一直在团队内部做推广时我比较推荐的接入场景有三类一是外部依赖比较多的页面比如嵌入了地图、客服、统计、广告这类三方脚本的站点真实环境里的加载顺序问题非常常见二是涉及文件上传、长列表渲染、WebSocket 通信等复杂交互的页面过程长、状态多、问题难复现三是上线窗口小、没有专职测试的小团队项目rea 可以作为一种廉价的质量观测兜底。不太推荐所有页面无脑接入。有些访问量巨大的落地页资源和请求数量极其庞大即使 rea 做了采样和容量控制仍然会带来持续的开销。这种情况下更合理的做法是只在灰度环境或者调试参数触发时启用而不是常驻所有用户。5.3 一些个人经验整理最后分享几个我在实际使用中沉淀下来的经验不一定都写在代码里但每个都是真金白银换来的。第一如果要做成本地数据方案一定要设置总配额并写好淘汰策略不然用户浏览器存储被你的日志塞满早晚会被投诉。第二在数据导出格式上尽早统一 JSON Schema因为 rea 的分析器和生成器需要对齐字段改版本号很容易但历史报告的兼容性必须提前考虑。第三别把 rea 做成一个人的工具我有几次排查问题后是让后端同事也打开导出 JSON 看的发现对方也能快速定位一些接口侧的问题。把数据格式设计得通用、清晰工具的受众就会超过前端团队本身。接下来我会继续把 rea 的离线聚合和规则判断补完如果你也在做类似的东西或者在排查线上问题时有什么独门思路欢迎看完文章后自己去折腾一个适合自己的版本。排查问题的工具不在多关键是在出事那一刻你手里有足够的过程数据可供回放。