JSON录制回放优化:解决体积膨胀、回放卡顿与密码泄露

JSON录制回放优化:解决体积膨胀、回放卡顿与密码泄露 做用户操作录制回放结果录下来的 JSON 比视频还大回放时卡成幻灯片更尴尬的是用户密码直接明文写在快照里。这个问题听起来像段子但在自动化测试、前端监控、用户行为分析团队里并不少见。这篇文章就围绕这个场景拆开讲JSON 录制回放为什么会体积膨胀、回放卡顿的瓶颈在哪里、密码明文记录的安全隐患怎么解决以及录制层、回放层、批量处理层的实际优化做法。文章会给 JSON 数据格式示例、事件采集脱敏代码、批量压缩脚本和回放调度思路适合正在自研录制回放工具的开发、测试和运维同学参考。这类问题的核心不是“要不要做录制回放”而是“怎么在采集、存储、重放三个环节做取舍”。如果把所有事件都原样塞进 JSON体积一定大如果回放时一次性把几万条事件全部塞进 DOM浏览器一定会卡。密码问题则说明采集规则缺少最基本的字段过滤。这三个问题可以分开解决也可以做成一整套优化流程。下面按实际工程中最常见的顺序来展开。1. 录制回放方案核心能力速览JSON 事件流录制回放通常用于用户行为分析、自动化测试和线上问题复现。它和录屏视频方案是两种完全不同的技术路线先给出一张对比表方便后续分析时定位问题维度JSON 事件流录制录屏视频录制存储内容事件、DOM 变更、坐标、输入值等连续画面帧体积增长速度与事件频率、字段数量、快照策略强相关与时长、分辨率、码率强相关回放能力可定位到具体 DOM 节点可继续注入操作只能被动观看体积优化空间可采样、压缩、差分快照、二进制化主要靠降低码率和分辨率安全风险若不脱敏密码、手机号等可能明文入 JSON同样可能录到密码但不太会进入结构化日志回放性能风险DOM 重建、事件批量触发容易卡顿视频解码压力相对可控从表格可以看出JSON 录制回放的上限更高但需要处理的问题也更多。标题里的“JSON 比视频还大”和“卡成幻灯片”本质上都是没有做好录制层的减量和回放层的渲染调度。2. 适用场景与使用边界JSON 录制回放适合以下场景自动化测试中的操作回放例如登录、下单、配置流程的反复验证。问题复现当线上用户反馈异常时通过回放用户操作路径还原现场。用户行为分析统计点击热区、表单流失点、异常路径。前端性能诊断观察特定操作前后的页面状态变化。不适合的场景也很明显需要绝对真实连续画面的场景、长时间全量录制所有用户操作的场景、以及未获得用户授权的采集场景。录制用户操作本质上是一种比埋点更敏感的行为采集如果没有明确告知和授权即使技术上做得很完善也仍然存在合规风险。如果项目不是内部测试环境而是面向真实用户的产品功能必须先经过法务和隐私团队评审。使用边界还有一条不要试图用 JSON 事件流去替代视频监控类需求。事件流回放的目标是“还原关键操作路径”而不是“保存每一帧渲染结果”。如果业务方要求“用户看到什么回放就得看到什么”那更适合用录屏方案或者把录屏和 JSON 事件流结合视频负责画面JSON 负责可检索、可跳转、可注入的操作元数据。3. 现象拆解JSON 体积膨胀与回放卡顿的原因3.1 JSON 为什么比视频还大JSON 事件录制的体积失控通常是几个因素叠加的结果。第一是高频事件被全量记录。鼠标移动、滚动、输入框按键这些事件本身触发频率很高如果每次触发都生成一条完整 JSON那么一小时内的数据量会非常可观。事件越密集JSON 数组越长。第二是单条事件携带了过多冗余字段。比如记录 click 事件时把 event 对象的所有属性都序列化包括 target、currentTarget、timeStamp、clientX、clientY、layerX、offsetX、path、composedPath 等。其中很多字段在回放时根本用不到但它们会显著拉长 JSON 体积。第三是快照策略太激进。很多录制方案为了回放时准确还原页面会在事件前后保存 DOM 快照。如果每次事件都保存一次全量 HTML页面结构稍微复杂一点体积就会爆炸。全量快照之间的重复内容非常多这些重复就是纯浪费。第四是 JSON 格式自身的膨胀。JSON 是文本格式字段名会重复出现。一个包含几十个字段的对象如果存了几万条字段名就占了一大部分体积。再加上没有压缩最终文件往往比视频还大。要控制 JSON 体积就必须从源头减少无意义事件精简字段用差分代替全量快照并在落盘前做压缩。这些方案后面会展开。3.2 回放为什么会卡成幻灯片回放卡顿和录制体积大有一定关系但更本质的原因是回放算法没有做调度。JSON 事件回放的常见实现是读取全部事件遍历数组按时间顺序把每个事件对应的 DOM 操作一次性执行。这种“一次性全量执行”的方式在事件数量少时没问题但事件达到几万条时浏览器主线程会被大量 DOM 操作瞬间占满页面自然会卡成幻灯片。另一个原因是回放在执行事件时没有做批量收集和批量渲染。比如一个 input 事件可能会触发一次 value 赋值、一次文本节点更新、一次样式变化如果录制时把这些都记成了相互独立的事件回放就会反复触发重排和重绘。浏览器在短时间内收到大量渲染任务帧率就会骤降。还有一个隐藏问题是快照恢复的开销。如果每次回放都要把录到的全量 HTML 塞进容器当 HTML 很大、层级很深时innerHTML 赋值和后续样式计算都会非常耗时。这也是为什么回放尽量用差分快照而不是每次全量重建 DOM。回放优化可以从两个方向入手一是把事件按帧率分批次执行避免一次性阻塞主线程二是在结构上只恢复变化的部分尽量减少重排和重绘。4. 用户密码直接展示在 JSON 中的风险与规避思路录制回放的 JSON 里出现用户密码核心原因是 input 事件采集做了“通吃”。用户在密码框里每按一个键都会触发 input 事件而事件对象里通常带有本次输入的值采集脚本如果直接把 event.target.value 序列化进日志密码就明明白白地写进了 JSON。这带来的风险不只是展示问题而是真实的数据泄露录制日志如果上传到日志平台密码会以明文形式落库。如果日志文件被导出或同步到第三方平台密码会随之泄露。即使不落库开发调试时打印日志也可能被截图传播。从合规角度看明文记录用户口令属于高风险行为一旦出问题就是安全事故。规避思路必须分成前端采集、服务端落库、日志导出三层。前端采集层要做字段识别。在 event 处理里判断触发元素是否为密码框、敏感输入框或带>{ type: click, ts: 1710000000000, target: #login-button, x: 120, y: 45 }这个示例只保留了回放和统计最需要的字段事件类型、时间戳、目标选择器、坐标。相比序列化整个 event 对象体积会小一个数量级。目标选择器也不建议每次都用 JavaScript 去生成可以在采集时做一次层级精简优先使用 id 或>const observer new MutationObserver((mutations) { const changes mutations.map((mutation) { const record { type: mutation.type, target: getSelector(mutation.target), }; if (mutation.type attributes) { record.attrName mutation.attributeName; record.attrValue mutation.target.getAttribute(mutation.attributeName); } if (mutation.type childList) { record.addedNodes mutation.addedNodes.length; record.removedNodes mutation.removedNodes.length; } return record; }); buffer.push(changes); }); observer.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true, });需要注意的是MutationObserver 记录的是 DOM 变化但不会自动包含用户输入的 value 变化。对 input 事件的 value 记录仍然需要单独实现同时要做脱敏。差分快照的优点是回放时不需要每次重建整棵 DOM缺点是事件量会变多。所以差分记录也需要合并同一节点在极短时间内发生多次变更可以合并成一次最终状态。5.3 压缩存储与二进制序列化录制出来的 JSON 基本都可以压缩。最直接的方式是 gzip 或 brotli 压缩后存储。文本类 JSON 的重复度很高压缩收益非常明显。如果需要进一步压缩可以把字段名缩短比如用t代替type用ts代替timestamp。但字段名缩短会降低日志可读性建议在采集端使用短字段名在分析端再映射回来或者直接接受短字段名。如果项目对体积要求极其严格可以考虑用 MessagePack、CBOR 这类二进制序列化格式代替 JSON。二进制格式没有文本引号和字段名重复占用体积通常比 JSON 小很多。但二进制格式不方便人工查看和排查问题所以实际项目里可以做成双层策略实时采集走二进制压缩导出调试时再转成 JSON。下面是一段压缩 JSON 文件并保留原始文件名的 Python 脚本示例实际使用时按目录调整即可import gzip import json from pathlib import Path def compress_json_log(source: Path): with source.open(r, encodingutf-8) as f: data json.load(f) target source.with_suffix(.json.gz) with gzip.open(target, wt, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse) print(fcompressed: {source.name} - {target.name}, size{target.stat().st_size})这个脚本只负责压缩不含脱敏。生产环境里应该先脱敏再压缩否则即使压缩了解压后密码仍然是明文。6. 回放层优化分帧调度与虚拟时间回放卡顿的解决方法是把“一次性执行全部事件”改成“按帧调度执行”。浏览器的渲染帧率大概是每秒 60 帧也就是每帧大约 16.7 毫秒。如果一帧内要做 100 个 DOM 操作必然卡顿。所以在回放循环里每执行一批事件后应该让出主线程给渲染层。最简单的方式是使用 requestAnimationFrame把事件队列按照帧率分批执行。下面是一个简化的分帧回放示例function replayEvents(events, container) { let index 0; function applyBatch() { const startTime performance.now(); while (index events.length performance.now() - startTime 8) { const event events[index]; applyEvent(event, container); index 1; } if (index events.length) { requestAnimationFrame(applyBatch); } else { console.log(replay finished); } } requestAnimationFrame(applyBatch); }这个示例把每一帧的处理时间限制在 8 毫秒左右给浏览器渲染留出剩余时间。真实项目里需要根据事件复杂度动态调整阈值不能写死但思路是一致的不让主线程一次性执行过长时间。虚拟时间也是回放性能优化的重要手段。录制回放里经常出现用户在某一步停留了 10 秒这 10 秒可能只产生了一条事件。如果回放时严格按真实时间推进用户要等 10 秒才能看到下一步。更好的做法是回放时使用虚拟时钟空闲时间段快速跳过只在有事件变化的节点暂停或慢速播放。这样可以大幅缩短回放总时长也减少了持续渲染的时间。除此之外在回放前把事件列表按时间戳排好序避免录制乱序导致 DOM 反复回退。回放容器尽量只在事件发生时更新不要为了性能把整页重新注入。7. 敏感数据脱敏代码示例脱敏不能只靠口头约定要落到代码里。前端采集时识别敏感输入框输出脱敏后的 JSON服务端落库前做二次清洗。下面是一个基于事件委托的敏感输入脱敏示例const SENSITIVE_SELECTOR [ input[typepassword], input[name*password i], input[name*pwd i], input[autocompletecurrent-password], input[data-privatetrue] ]; function isSensitiveField(target) { return target target.matches target.matches(SENSITIVE_SELECTOR.join(,)); } function collectInputEvent(event) { const target event.target; const payload { type: input, ts: Date.now(), target: getSelector(target), value: isSensitiveField(target) ? [REDACTED] : target.value }; buffer.push(payload); } document.addEventListener(input, collectInputEvent, false);这里面的getSelector函数需要自行实现核心逻辑是生成一个能唯一定位节点且尽量短的选择器。服务端做二次清洗时可以用递归方式扫描 JSON 中的敏感字段名发现password、pwd、secret等 key 时把值替换掉。下面是一个 Python 示例import re import json SENSITIVE_KEYS re.compile(r(password|passwd|pwd|secret|token)$, re.IGNORECASE) def mask_sensitive(obj): if isinstance(obj, dict): for key, value in list(obj.items()): if SENSITIVE_KEYS.search(key): obj[key] [REDACTED] else: mask_sensitive(value) elif isinstance(obj, list): for item in obj: mask_sensitive(item) return obj def load_and_mask(path): with open(path, r, encodingutf-8) as f: data json.load(f) return mask_sensitive(data)这个示例是常用的兜底逻辑但要注意它无法覆盖所有情况比如把一个密码字段命名为content就无法识别。所以最好的策略是前端标记优先服务端兜底补充。8. 批量处理与 API 接入录制回放日志积累到一定规模后会面临一个批量处理问题历史日志里可能有大量未脱敏数据需要清洗每天新增的日志需要压缩归档回放工具需要按用户、时间段、事件类型检索。批量处理不复杂关键是加日志、失败重试和幂等。下面是一个批量处理目录下所有 JSON 日志的 Python 模板import json import gzip from pathlib import Path def process_all_logs(input_dir: Path, output_dir: Path): output_dir.mkdir(parentsTrue, exist_okTrue) for src in input_dir.glob(*.json): try: data json.loads(src.read_text(encodingutf-8)) sanitized mask_sensitive(data) # 假设上面已定义 out output_dir / (src.stem .json.gz) with gzip.open(out, wt, encodingutf-8) as f: json.dump(sanitized, f, ensure_asciiFalse) print(f[OK] {src.name} - {out.name}) except Exception as e: print(f[FAIL] {src.name}: {e}) process_all_logs(Path(./raw_logs), Path(./processed_logs))批量处理要保证输出目录可重跑。如果某次处理到一半程序崩溃重新执行时应该跳过已经处理完的文件或者允许覆盖但不报错。可以按文件名后缀加.done标记来记录处理状态。如果清洗服务要同时给多个采集端用可以封装成 HTTP 接口。以 Flask 为例一个最简单的脱敏接口如下from flask import Flask, request, jsonify import gzip import json app Flask(__name__) app.route(/sanitize, methods[POST]) def sanitize(): raw request.get_data() data json.loads(gzip.decompress(raw) if raw[:2] b\x1f\x8b else raw) sanitized mask_sensitive(data) return jsonify({ok: True, data: sanitized}) if __name__ __main__: app.run(host127.0.0.1, port8000)这个接口仅供本地或内网调用生产环境必须加认证、限流和访问控制。不要把这个接口暴露到公网否则任何人都可以上传 JSON 文件请求清洗既浪费资源也存在数据二次暴露风险。9. 资源占用与性能观察方法优化做完后要有一个可量化的观察方法不能只靠“感觉不卡了”。录制层重点看三个指标JSON 文件体积文件越大说明事件冗余越多。单条事件平均字段数字段数多说明采集过于粗暴。快照频率看同一页面在一个会话中出现几次全量快照。回放层重点看三个指标帧率开发者工具 Performance 面板可以看回放过程中的帧率变化。主线程阻塞时间长任务数量越多说明单次执行事件太重。内存占用标签页内存持续增长说明 DOM 节点或缓存没有被正确释放。性能观察可以用浏览器开发者工具也可以用 PerformanceObserver 在代码里采集基础指标。下面是一个最简单的帧率观察示例let frameCount 0; let lastTime performance.now(); function measureFps() { frameCount; const now performance.now(); if (now - lastTime 1000) { console.log(fps:, frameCount); frameCount 0; lastTime now; } requestAnimationFrame(measureFps); } requestAnimationFrame(measureFps);实际项目里可以把这个数据上报到监控平台。不要等到用户反馈“回放页面特别卡”才去排查应该在上线回放功能时就加性能监控。10. 常见问题排查表问题现象可能原因排查方式解决方案JSON 文件比视频还大记录了高频事件、全量快照、未压缩统计事件数量和单事件字段数事件采样、差分快照、gzip 压缩回放时页面卡死一次性执行大量 DOM 操作Performance 面板看长任务requestAnimationFrame 分帧调度回放时密码字段可见前端采集脱敏未生效检查录制的 JSON 内容增加敏感字段识别和脱敏逻辑回放位置错乱事件顺序未按时间排序检查事件时间戳回放前按 ts 排序回放后页面状态不一致快照保存不完整对比录制和回放后的 DOM 结构使用 MutationObserver 补充差异记录批量处理日志失败文件损坏或编码异常查看单文件异常日志增加异常捕获和跳过机制接口无法访问端口被占用或未启动检查服务日志和端口状态清理进程或更换端口压缩后文件仍然很大压缩前未做事件减量检查压缩前 JSON 结构先减事件再压缩问题排查时优先看原始 JSON 的前几百条记录基本能判断是事件过多、字段过多还是快照过多。不要上来就改算法先确定瓶颈在哪一层。11. 最佳实践与合规提醒录制回放功能要上线建议按下面的清单过一遍最小化采集只记录业务关键事件不记录鼠标轨迹等高频低价值事件。前端脱敏密码框、手机号、身份证等敏感输入框不记录具体值。服务端兜底落库前扫描敏感字段二次替换。访问控制回放日志文件和接口必须加权限不能匿名访问。数据保留期设置日志有效期超过保留期自动清理。用户授权真实用户行为录制前必须获得明确同意并在隐私政策中说明。测试环境验证在上线前用自动化脚本模拟各类输入确认敏感字段不会进入日志。日志审计定期抽查录制日志确认脱敏规则没有被绕过。如果项目已经上线且录到过明文密码不要简单认为“后续改掉就行”。需要评估历史日志的影响范围必要时通知相关用户修改口令同时检查日志平台、备份存储、第三方分析系统是否仍有残留。安全边界不能只在设计文档里写要落到采集代码和存储逻辑里。用户密码直接展示在 JSON 中这类问题一旦发生就是事故级别的隐患修复成本远高于一开始就加上脱敏逻辑。12. 总结与下一步先不要着急优化回放渲染第一步应该做一次“事件盘点”打开一份录制的 JSON看看里面到底存了哪些事件、哪些字段。如果能看到password或用户输入的明文内容先加脱敏这是最高优先级。然后再做事件减量和压缩把 JSON 体积降下来。最后再回头看回放卡顿用分帧调度和虚拟时间解决渲染瓶颈。从这个项目标题延伸出来的问题本质上是录制回放系统里最常见的三个坑采集无节制、回放无调度、脱敏无兜底。把这三个坑填上JSON 不会比视频大回放不会变成幻灯片密码也不会出现在日志里。建议先从这个最小闭环做起脱敏 - 压缩 - 分帧回放。跑通之后再逐步完善批量处理和接口化服务就可以把这套能力接到测试平台或用户行为分析系统里了。