《互联网产品的客户端架构·端形态与体验基础》附录 B 端上问题排查手册

按用户可描述的症状组织的端上排查手册:白屏、首屏慢、滚动卡顿、内存增长、请求失败、状态丢失、崩溃、深链打不开与版本兼容异常,每种症状都给出按概率排序的原因、验证步骤、处置建议和需要补的观测,并附从产物到渲染的跨症状排查顺序、现场记录模板与常见误判。

这本手册怎么用

排查端上问题的难点,不是不知道有哪些原因,而是不知道先看哪里。同一个“页面打不开”,可能是资源 404、接口超时、权限重定向、容器拦截,也可能是旧版本不支持新语法。没有顺序,就会在十几个可能原因之间来回跳。

这份手册按用户能描述的症状组织,而不是按技术模块组织。用户不会说“我的 chunk 加载失败”,只会说“白屏”。每个症状给出四块内容:按概率排序的原因、从外到内的验证步骤、先止血再根治的处置建议、以及这次排查后应该补上的观测。概率排序是经验判断,不是统计结论,实际项目要结合自己的版本与设备分布调整。

手册里的耗时、比例和阈值都是案例假设值,用来演示判断过程。排查时以现场证据为准,不要用一个假设值去否定另一个假设值。

症状总表

症状首先怀疑关键证据首查位置
白屏产物未加载、根组件异常资源 404、控制台报错发布产物与错误边界
首屏慢关键资源串行、接口瀑布瀑布图、LCP 元素网络面板与渲染路径
滚动卡顿长任务、全量渲染长任务、帧耗时主线程与列表实现
内存增长监听未清理、缓存无上限堆快照、增长趋势页面生命周期与缓存
请求失败网络切换、鉴权、超时错误码、耗时分布数据层与网关
状态丢失进程回收、路由卸载生命周期日志、存储快照状态归属与持久化
崩溃空值、异步异常、OOM堆栈、设备分布崩溃平台与符号表
深链打不开路由未注册、登录拦截落地页、参数丢失路由表与入口配置
版本兼容异常字段变更、schema 漂移旧版本错误率契约与版本矩阵

白屏

可能原因(按概率排序)

排序原因为什么常见支持的证据
1静态资源未加载或 chunk 404发布时 index 与资源版本错配、CDN 缓存首文档 200,但 JS/CSS 404
2根组件之前抛出异常初始化代码、Polyfill、第三方 SDK控制台有未捕获错误,挂载点为空
3首屏接口长时间阻塞页面用接口结果决定是否渲染请求 pending 或超时,骨架屏不消失
4挂载点缺失或被样式隐藏模板改动、容器高度为 0DOM 存在但不可见或选择器不匹配
5登录态失效导致重定向循环鉴权拦截与页面初始化互相等待URL 反复跳转,网络面板循环

验证步骤

  1. 先在出问题的版本与设备上复现,记录 URL、版本号、内核版本和网络类型。
  2. 打开网络面板,确认首文档状态码,再逐个检查 JS、CSS、字体是否 404 或被拦截。
  3. 查看控制台第一条错误,而不是最后一条。后续错误往往是第一条错误的连锁反应。
  4. 禁用缓存、换隐私窗口重试。若恢复正常,优先怀疑 CDN 与本地缓存版本错配。
  5. 关闭 JavaScript 或查看服务端返回的 HTML,判断是渲染路径问题还是客户端执行问题。

处置建议

  • 给入口 HTML 设置不缓存或短缓存,静态资源使用内容指纹,避免新旧资源互相引用。
  • 增加全局错误边界与路由级错误边界,确保异常时至少渲染可操作的兜底页。
  • chunk 加载失败时自动重载一次,并带上版本参数;仍失败则展示“刷新或升级”的明确指引。

需要补的观测

  • 白屏率:无 FCP、无根节点内容或错误边界触发的会话占比。
  • 资源加载失败率,按版本、CDN 节点、资源名分桶。
  • 未捕获异常率与首条错误信息,关联发布版本。
  • 首屏接口的 P95 与超时率,区分接口名。

首屏慢

可能原因(按概率排序)

排序原因为什么常见支持的证据
1关键资源体积过大或串行加载首屏依赖整包、入口未拆分瀑布图显示长串依赖
2首屏接口瀑布先取用户再取配置再取内容多个接口首尾相接
3TTFB 偏慢CDN 未命中、回源慢、服务端排队首字节时间占了大头
4渲染阻塞的脚本与样式同步脚本、未内联关键 CSS主线程长时间无法绘制
5图片未压缩或尺寸不当原图直出、缺少响应式图片LCP 元素是超大图片

验证步骤

  1. 确认“慢”的口径:是 LCP、FCP、可交互时间,还是用户主观等待。不同口径对应不同原因。
  2. 看瀑布图,标出首屏真正需要的资源,区分关键路径与非关键路径。
  3. 找到 LCP 元素,确认它是文本、图片还是视频封面,再判断优化方向。
  4. 检查首屏接口数量与依赖关系,确认是否存在可以并行或合并的请求。
  5. 分别看 TTFB、资源下载、脚本执行、渲染四段耗时,找出占比最大的一段。

处置建议

  • 按路由拆分代码,首屏只加载必要模块,非关键模块延迟加载。
  • 首屏接口并行化或聚合,避免“取 A 才能取 B”的串行结构。
  • 对 LCP 图片做尺寸适配、压缩与预加载,避免用原图撑首屏。
  • 关键 CSS 内联,非关键脚本延期,减少渲染阻塞。

需要补的观测

  • LCP P75,按路由、设备档位、网络类型分层。
  • TTFB 与资源下载耗时的分段统计。
  • 首屏接口数量、P95 与瀑布深度。
  • 缓存命中率与回源率。

滚动卡顿

可能原因(按概率排序)

排序原因为什么常见支持的证据
1长任务占用主线程大计算、复杂 diff、同步解析长任务与卡顿时间重合
2列表全量渲染一次渲染成百上千项DOM 节点数随数据量线性增长
3布局抖动或强制同步布局读写交替、滚动中测量帧内出现大量 layout
4图片解码与大图滚动时解码原图卡顿集中在图片进入视口时
5频繁状态更新导致整树重渲染全局状态驱动大范围更新渲染次数远高于交互次数

验证步骤

  1. 录一段性能时间线,确认卡顿是持续性的还是集中在某些操作。
  2. 看长任务列表,记录每个长任务的耗时与调用栈。
  3. 统计列表 DOM 节点数与数据量,判断是否需要虚拟化。
  4. 检查滚动回调里是否做测量、排序、过滤等重计算。
  5. 临时移除图片或动画,确认卡顿是否消失,缩小范围。

处置建议

  • 列表虚拟化或分页,只渲染视口附近的内容。
  • 把重计算移出主线程,或拆成可中断的小任务。
  • 滚动回调只做轻量记录,复杂逻辑防抖或延后。
  • 动画优先使用合成友好的属性,避免触发布局与重绘。

需要补的观测

  • 长任务数量与总阻塞时间,按路由分桶。
  • 帧耗时分布与慢帧率,按设备档位分层。
  • INP 高分位,关联具体交互类型。
  • 列表渲染数量与 DOM 节点数。

内存增长

可能原因(按概率排序)

排序原因为什么常见支持的证据
1监听、定时器、订阅未清理页面卸载后仍持有引用重复进出页面后内存阶梯上升
2缓存无上限图片、请求结果、路由缓存只增不减缓存条目数持续增长
3闭包持有大对象或 DOM回调捕获了不再需要的引用堆快照中 retainer 指向旧页面
4全局单例累积事件总线、日志队列、埋点缓冲全局对象体积随会话增长
5大图与 base64 常驻图片未释放、内联资源过多内存峰值与图片数量相关

验证步骤

  1. 明确“增长”是稳定占用高,还是随操作持续上升。前者可能是预算问题,后者更可能是泄漏。
  2. 重复进入、退出同一页面若干次,观察内存是否回到基线。
  3. 采集堆快照,对比两次快照之间的新增对象与 retained size。
  4. 检查页面卸载路径:监听、定时器、订阅、请求、动画是否都有对应的清理。
  5. 查看缓存实现,确认是否有容量上限、过期策略与淘汰机制。

处置建议

  • 把清理写进页面生命周期,提供统一的注册与释放接口,而不是依赖开发者自觉。
  • 缓存设置容量上限与淘汰策略,避免无界增长。
  • 大图使用后主动释放,避免长期持有 data URL。
  • 全局单例只保留必要状态,日志与埋点缓冲设置上限。

需要补的观测

  • 前台与后台内存峰值,按页面与设备档位分层。
  • OOM 与被系统杀死的比率,与普通崩溃分开统计。
  • 页面停留时长与内存增长的关联。
  • 缓存条目数与命中率,避免“用内存换命中”失控。

请求失败与超时

可能原因(按概率排序)

排序原因为什么常见支持的证据
1网络切换、DNS 或 TLS 失败移动网络不稳定、代理干扰失败集中在特定网络类型
2超时预算不合理预算短于真实尾延迟,或长到用户放弃超时率与 P95 接近预算
3重试无退避或去重失败后立即重试,形成风暴重试次数与失败率同时升高
4鉴权过期与刷新死循环多请求同时触发刷新401 反复出现,刷新接口也被拒
5服务端 5xx 或限流容量不足、发布异常错误码集中在 5xx 或 429

验证步骤

  1. 先看错误码分布,区分网络错误、超时、4xx、5xx 与业务错误。
  2. 看耗时分布,确认失败是“快速失败”还是“等到超时”。
  3. 按网络类型、版本、地域分层,确认是否集中在特定人群。
  4. 检查重试次数与请求去重,确认失败是否被重试放大。
  5. 对齐服务端日志与端上请求 ID,确认请求是否真正到达服务端。

处置建议

  • 把错误分成可重试、不可重试、结果未知三类,分别处理。
  • 重试使用指数退避加随机抖动,并设置总预算与最大次数。
  • 相同请求去重,共享进行中的 Promise 或等价机制。
  • 鉴权刷新单飞,避免多个请求同时刷新令牌。

需要补的观测

  • 请求成功率,明确分母是请求、会话还是用户。
  • 错误码分布与超时率,按接口与版本分桶。
  • 重试率、去重命中率与离线队列积压。
  • 端上耗时与服务端耗时的对照,找出差异段。

状态丢失

可能原因(按概率排序)

排序原因为什么常见支持的证据
1进程被系统回收后台停留、内存压力冷启动后状态归零
2路由切换卸载组件状态放在页面组件内返回后表单为空
3刷新或深链重建状态只存在内存刷新后回到默认值
4存储写入失败配额、权限、序列化异常本地存储无记录或旧值
5多标签或多实例竞争同一键被覆盖最后写入者获胜,内容错乱

验证步骤

  1. 明确“丢失”的是哪一类状态:草稿、滚动位置、筛选条件、登录态还是未发送消息。
  2. 复现路径要包含切后台、杀进程、刷新、返回、深链进入等不同入口。
  3. 查看生命周期日志,确认页面卸载与恢复时分别执行了什么。
  4. 检查本地存储快照,确认写入是否成功、键名是否稳定、值是否可反序列化。
  5. 在多标签或多实例下复现,确认是否存在并发覆盖。

处置建议

  • 把状态分级:可丢、可恢复、不可丢。不可丢的状态必须持久化并校验完整性。
  • 草稿自动保存,节流写入,恢复时提示“已恢复上次编辑”。
  • 路由切换时把需要保留的状态提升到路由层或持久层,而不是留在页面组件。
  • 存储写入失败要有降级与提示,不能静默吞掉。

需要补的观测

  • 状态恢复成功率,按入口(冷启动、深链、返回)分层。
  • 草稿丢失率与存储失败率。
  • 冷启动恢复耗时,避免恢复本身拖慢首屏。
  • 多标签冲突次数与覆盖告警。

崩溃

可能原因(按概率排序)

排序原因为什么常见支持的证据
1空值与类型假设错误接口字段缺失、可选值未处理堆栈指向属性访问
2异步异常未捕获Promise、回调、定时器堆栈无明确业务入口
3内存 OOM大图、缓存、泄漏累积崩溃前内存接近上限
4原生或 WebView 兼容问题旧系统、旧内核、厂商定制集中在特定机型与系统版本
5第三方 SDK 异常广告、统计、推送堆栈落在 SDK 包名
6并发与时序问题多线程、竞态、重复初始化偶发,难以稳定复现

验证步骤

  1. 先确认崩溃类型:脚本异常、原生崩溃、OOM、ANR 还是被系统杀死。
  2. 看聚合后的堆栈,找最内层的业务代码帧,而不是最外层的框架帧。
  3. 按版本、机型、系统版本、页面分层,确认是否集中在特定组合。
  4. 检查符号表是否完整,未符号化的堆栈只能看到地址,无法归因。
  5. 用相同数据与操作路径尝试复现,记录前置状态与网络条件。

处置建议

  • 对外部数据做边界校验,接口字段缺失时使用默认值并记录告警。
  • 全局捕获未处理异常,异步入口统一包装,避免异常穿透到进程层。
  • 错误边界按路由与关键模块划分,单点失败不要拖垮整个应用。
  • 第三方 SDK 隔离初始化,失败时降级而不是阻断主流程。

需要补的观测

  • 崩溃率,明确按会话还是按用户统计,并保留启动次数分母。
  • Top 崩溃堆栈与影响用户数,按版本与机型分桶。
  • OOM 率与 ANR 率,单独统计,不与普通崩溃混合。
  • 崩溃前的关键路径:页面、操作、网络、内存。

深链打不开

可能原因(按概率排序)

排序原因为什么常见支持的证据
1路由未注册或路径不匹配大小写、尾斜杠、参数格式命中兜底页或首页
2登录拦截丢失目标跳登录时未保留原始地址登录后回到首页
3参数编码错误中文、特殊字符、嵌套 URL参数被截断或乱码
4版本不支持该路由旧客户端没有新页面仅旧版本失败
5入口配置缺失Universal Link、App Link、Scheme浏览器打开而不是应用
6冷启动与热启动处理不一致两套入口逻辑热启动正常,冷启动失败

验证步骤

  1. 分别从浏览器、扫码、推送、站外链接进入,确认是否所有入口都失败。
  2. 分别测试冷启动与热启动,确认两条初始化路径是否一致。
  3. 分别在登录与未登录状态测试,确认登录回跳是否保留目标。
  4. 检查原始链接与最终落地页,逐段对比参数是否丢失或变形。
  5. 在旧版本上测试,确认是否为新路由能力缺失。

处置建议

  • 建立路由白名单与显式兜底页,未匹配时给出可操作提示,而不是静默回首页。
  • 登录拦截保存完整目标地址,登录成功后原样回跳。
  • 统一参数编解码与规范化规则,对中文和嵌套 URL 做专门测试。
  • 新旧版本通过能力探测或版本协商决定落地页,不支持时引导升级。

需要补的观测

  • 深链到达率:从点击到目标页面成功渲染的比例。
  • 失败原因分布:路由未匹配、登录拦截、参数错误、版本不支持。
  • 各入口的到达率对比:浏览器、扫码、推送、站外。
  • 登录回跳成功率。

版本兼容异常

可能原因(按概率排序)

排序原因为什么常见支持的证据
1服务端删除或改语义字段新接口未保留旧字段旧版本解析失败或显示空
2枚举与可空性理解不一致新增枚举值、字段变为可空旧版本落入未知分支
3本地缓存 schema 漂移旧数据被新代码读取升级后首次启动异常
4资源与入口版本错配CDN 缓存、灰度混合新旧资源交叉引用
5缺少强制升级兜底旧版本无法完成核心流程旧版本错误率持续升高

验证步骤

  1. 明确兼容窗口:当前需要支持多少个旧版本、多长时间。
  2. 用旧版本客户端回放核心流程,确认登录、首屏、下单等路径是否可用。
  3. 对比接口契约变更,检查字段删除、类型变化、枚举新增与可空性。
  4. 清空本地缓存与保留旧缓存各测一次,确认 schema 迁移是否正常。
  5. 检查灰度与 CDN 配置,确认入口与资源版本是否一致。

处置建议

  • 接口遵循向后兼容:新增字段可选、旧字段保留、枚举新增有默认分支。
  • 客户端对未知字段与未知枚举做容错,不因新值崩溃或白屏。
  • 本地数据带 schema 版本,升级时执行迁移,失败时安全重建。
  • 入口 HTML 与静态资源版本绑定,避免灰度期间交叉引用。

需要补的观测

  • 版本分布与旧版本留存曲线。
  • 旧版本错误率、白屏率与核心流程成功率。
  • API 版本命中分布与契约告警。
  • 缓存迁移失败率与升级后首次启动异常率。

跨症状的排查顺序

单个症状内部有顺序,多个症状并存时也需要顺序。建议始终从外向内:

  1. 先确认发布与产物:版本号、入口 HTML、静态资源、CDN 与灰度状态。产物错了,后面所有分析都建立在错误前提上。
  2. 再确认入口与路由:用户是从哪里进来的,落到哪个页面,参数是否完整。入口错了,页面本身的代码可能完全没问题。
  3. 然后确认网络与数据:请求是否发出、是否到达、返回什么、耗时多少。数据错了,渲染层只能呈现错误。
  4. 接着确认渲染与状态:组件是否挂载、状态是否恢复、错误边界是否触发。
  5. 最后确认设备与系统:内核、内存、权限、后台限制。前四层都正常时,问题往往在环境差异。

这个顺序不能保证一次命中,但能避免“在业务代码里找了半天,最后发现是 CDN 缓存了旧 index”。

记录模板

每次排查至少留下一份可复盘的记录。结构不必复杂,但要能让别人按同样步骤复现:

症状:
首次发现时间 / 版本 / 平台:
影响面(用户数、会话数、版本、设备、地域):
复现步骤:
关键证据(截图、日志、堆栈、请求 ID):
根因:
临时处置:
长期修复:
需要补的观测:
负责人 / 截止时间:

记录里最重要的是“关键证据”和“需要补的观测”。前者让结论可验证,后者让同类问题下次更早被发现。只写“已修复”的记录,对团队没有积累价值。

常见错误与边界

  • 跳过发布与产物检查,直接读业务代码。大量白屏与兼容问题根因在产物层。
  • 只看最后一条错误。第一条错误往往才是根因,后面的是连锁反应。
  • 用单台设备复现失败就下结论。端上问题高度依赖设备、网络与版本组合。
  • 把超时当成失败。超时的请求可能已经成功,写路径要按“结果未知”处理。
  • 只修表象不补观测。同样的问题会在下一个版本以另一种症状回来。
  • 把概率排序当统计结论。它只是排查起点,现场证据优先。
  • 忽略旧版本。兼容窗口内的旧客户端仍然是真实用户。
  • 排查记录只写结论。没有证据与复现步骤,结论无法被复核。

小结

端上排查的核心不是记住所有原因,而是建立稳定的顺序:先产物与入口,再网络与数据,然后渲染与状态,最后设备与系统。每个症状都按这个顺序走一遍,能排除大部分误判。

比修复更重要的是补观测。每一次排查暴露的盲区,都应该变成下一次的指标、日志或告警。附录 A 定义了指标口径,这份手册把口径用到具体症状上;下一份附录则把两者收进评审清单,让问题在发布前被问到。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查