怎么用这份手册
这份手册按「用户看到的症状」组织,而不是按模块组织。线上排查时,你通常先拿到一句模糊的反馈——「列表里有重复」「消息不更新了」「角标一直是 3」——然后才需要定位到模块。
每条症状给出四段:
- 可观察信号:不依赖用户描述,能自己复现或从日志里看到的证据。
- 常见根因:按出现频率排序,不是穷举。
- 验证步骤:用最小成本确认是哪一个根因。
- 修复方向:修哪里,以及修完要补什么测试。
手册假设你已经读过正文对应章节。它不重复原理,只给排查路径。
症状:信息流出现重复卡片
可观察信号:同一条内容在相邻两屏出现两次;下拉刷新后重复消失;接口返回的 id 在两次分页中有交集。
常见根因:
- 游标只用了排序字段的一部分,例如只用
created_at,同一毫秒的多条内容被重复返回。 - 客户端没有已见集合,直接追加分页结果。
- 刷新与加载更多并发,刷新返回的旧数据覆盖了分页的新位置。
- 容器淘汰了条目,但已见集合仍保留 id,导致重新拉取时被误判为重复(这是「越滑越少」,与重复相反)。
验证步骤:
- 抓两次分页的响应,比较返回 id 的交集。
- 在端上打印
appendPage的{ added, duplicates, dropped }。 - 检查游标编码是否包含唯一字段。
修复方向:服务端用 (created_at, id) 复合游标;客户端保留已见集合并与淘汰同步清理;刷新与分页串行化或按版本号丢弃过期响应。
症状:信息流位置跳动
可观察信号:用户滑到第 20 条,刷新后回到第 1 条;或插入新内容后视觉位置突然下移。
常见根因:
- 刷新替换了整个数组,而不是把新条目插到顶部。
- 列表没有稳定的 key,重新渲染时复用了错误的节点。
- 新内容插入到顶部但滚动位置没有补偿。
- 图片加载完成后高度变化,把下方内容顶走。
验证步骤:
- 记录刷新前后的第一条可见 id。
- 检查刷新逻辑是
replacePage还是重建数组。 - 检查列表 key 是否使用内容 id。
修复方向:刷新只插入新条目,已有条目保持原位;用 id 作为 key;插入到顶部时按插入高度补偿滚动;媒体预留宽高比。
症状:详情页白屏
可观察信号:进入详情页后长时间空白;控制台有请求失败;从分享链接进入必现,从列表进入偶现。
常见根因:
- 详情页依赖列表传入的完整对象,深链进入时对象不存在。
- 正文渲染等待评论接口,评论失败导致整页失败。
- 内容处于审核态,端上直接返回空。
- 路由参数解析失败,id 为空。
验证步骤:
- 直接用分享链接打开,看是否复现。
- 查看网络面板,确认是哪个请求失败。
- 检查页面 loading 状态是否覆盖了正文与评论两块。
修复方向:详情只接收 id,自己拉数据;正文与评论使用独立 loading 与错误边界;审核态渲染占位而不是空白;对无效 id 给出明确错误页。
症状:图片裂图或一直灰块
可观察信号:部分图片显示占位不消失;弱网下更明显;切换页面后恢复。
常见根因:
- 原图请求超时,但没有降级到缩略图。
- 图片 URL 带过期签名,缓存命中旧链接。
- 懒加载的可见性判断失效,图片从未进入视口。
- 加载失败后没有重试,占位状态被永久保留。
验证步骤:
- 查看图片请求的状态码与耗时。
- 检查占位组件是否有
onError处理。 - 检查 URL 是否带时间戳参数导致缓存失效。
修复方向:按「缩略图 → 原图」降级;失败重试一次后显示稳定占位;懒加载用 IntersectionObserver 并设置根边距;签名 URL 的过期时间要覆盖缓存周期。
症状:点赞数回退
可观察信号:点完赞数字 +1,几百毫秒后跳回原值;或刷新后与自己看到的不一致。
常见根因:
- 乐观更新后,服务端返回的旧计数覆盖了本地状态。
- 请求失败触发了回滚,但用户没有看到失败提示。
- 同一内容在列表与详情各有一份状态,只更新了其中一份。
- 计数更新用了绝对值而不是增量。
验证步骤:
- 打印请求的发出与返回时间,确认覆盖顺序。
- 检查回滚逻辑是否只改对应条目。
- 检查列表与详情是否共享同一个计数来源。
修复方向:服务端返回新计数而不是旧快照;用增量更新;以内容 id 为键维护单一计数源;失败时给出可恢复提示。
症状:评论发不出去
可观察信号:点击发送后按钮一直转圈;或提示失败但草稿消失。
常见根因:
- 发送期间没有禁用按钮,重复提交被服务端拒绝。
- 失败后清空了输入框,用户需要重打。
- 评论列表按时间倒序,新评论插入位置错误,用户以为没发出去。
- 审核拦截返回了通用错误,端上无法给出具体原因。
验证步骤:
- 检查请求是否重复发送。
- 检查失败分支是否保留草稿。
- 检查新评论的插入位置与滚动。
修复方向:发送中禁用并显示状态;失败保留草稿并提供重试;本地先插入「发送中」占位,成功后替换;审核失败给出具体文案。
症状:上传卡在 99%
可观察信号:进度条接近完成但永不结束;刷新后可续传;服务端收到全部分片但没有完成记录。
常见根因:
- 进度按「已发送字节」计算,最后一个分片的确认还没回来就显示 99%。
- 完成请求(合并分片)失败,端上没有重试。
- 某个分片被服务端判定失败,但客户端没有收到。
- 并发分片乱序完成,完成判定按顺序假设。
验证步骤:
- 打印每个分片的发送与确认时间。
- 检查完成请求是否发出、返回什么。
- 检查服务端的分片清单与客户端的已确认清单是否一致。
修复方向:进度按「已确认分片」计算;完成请求失败要重试;用上传会话 id 对账;完成判定等待所有分片确认。
症状:实时消息延迟
可观察信号:对方已发送,自己几秒后才看到;刷新页面能立刻看到。
常见根因:
- 连接已断,但端上仍显示在线,消息走的是轮询兜底。
- 心跳间隔过长,半开连接未被发现。
- 服务端推送队列积压。
- 端上收到消息后进入了一个低优先级队列,渲染被推迟。
验证步骤:
- 查看连接状态与最后一次收到心跳的时间。
- 对比推送到达时间与渲染时间。
- 检查是否有大量消息在同一时刻到达。
修复方向:心跳 + pong 超时检测;断线后立即降级到轮询或提示;消息到达即写入状态,渲染异步化但不延迟状态更新。
症状:重连后消息丢失
可观察信号:断网几十秒后恢复,中间的几条消息永久缺失;重新进入会话才补上。
常见根因:
- 重连时没有携带上次收到的序列号。
- 服务端忽略补拉参数,只推送新消息。
- 客户端把重连当成「全新会话」,清空了本地消息。
- 补拉只取「最新一页」,没有覆盖断线区间。
验证步骤:
- 抓重连请求,确认是否带
lastSeq。 - 对比服务端返回的第一条与本地最后一条。
- 检查重连逻辑是否清空状态。
修复方向:重连携带序列号;服务端从序列号之后补发;客户端保留已有消息并检测空洞;补拉失败时提示用户手动刷新。
症状:未读数不准
可观察信号:角标显示 3,点进去只有 1 条;或已读后角标不消失;多端数字不一致。
常见根因:
- 多个来源(点赞、评论、关注)各自维护计数,合并时重复累加。
- 已读回执只发给了其中一个端。
- 端上用本地累加而不是服务端返回的总数。
- 推送到达但应用未打开,角标没有更新。
验证步骤:
- 对比服务端返回的未读总数与端上显示。
- 检查已读回执的发送时机与目标。
- 检查合并逻辑是否去重。
修复方向:以服务端总数为准,本地只做增量;已读回执带设备 id;多端以最后已读位置为准;应用启动时强制同步一次。
症状:审核中的内容闪现
可观察信号:打开详情页先看到正文,几百毫秒后被替换成「审核中」。
常见根因:
- 列表返回的条目没有带审核状态,详情请求才知道。
- 端上先渲染缓存快照,未校验状态。
- 审核状态与正文在同一个请求里,但端上先渲染了部分字段。
验证步骤:
- 检查列表响应是否包含
state。 - 检查缓存是否记录了状态与时间。
- 检查渲染是否在状态确认前发生。
修复方向:列表与详情都返回审核状态;缓存快照带状态;状态未知时先渲染占位,确认后再渲染正文。
症状:搜索联想串结果
可观察信号:输入「abc」后显示的是「ab」的结果;快速输入时结果闪烁;清空输入后仍有旧结果。
常见根因:
- 请求并发返回,旧请求后到覆盖了新结果。
- 防抖只做了延迟,没有取消在途请求。
- 结果没有按查询词做键,缓存串用。
- 输入法组合期间触发了查询。
验证步骤:
- 打印每次请求的查询词与返回顺序。
- 检查是否使用
AbortController。 - 检查缓存 key 是否包含完整查询词。
修复方向:每次输入取消上一次请求;结果按查询词与版本号匹配后才渲染;组合输入期间不触发;缓存 key 包含查询词与筛选条件。
一张总表
| 症状 | 第一检查点 | 高频根因 |
|---|---|---|
| 重复卡片 | 分页 id 交集 | 游标不唯一 / 无已见集合 |
| 位置跳动 | 刷新前后首条 id | 整表替换 / 无稳定 key |
| 详情白屏 | 深链是否复现 | 依赖列表对象 / 错误边界过大 |
| 图片灰块 | 图片请求耗时 | 无缩略图降级 / 无 onError |
| 点赞回退 | 请求返回顺序 | 旧快照覆盖 / 绝对值更新 |
| 评论失败 | 草稿是否保留 | 失败清空 / 重复提交 |
| 上传卡 99% | 分片确认清单 | 进度语义错 / 完成请求未重试 |
| 消息延迟 | 连接状态与心跳 | 半开连接 / 推送积压 |
| 重连丢消息 | 重连是否带序列号 | 无补拉 / 清空会话 |
| 未读数不准 | 服务端总数对比 | 多来源累加 / 已读未同步 |
| 审核态闪现 | 列表是否带状态 | 状态后到 / 缓存无状态 |
| 联想串结果 | 请求返回顺序 | 无取消 / 缓存 key 不全 |
排查纪律
这份手册最容易误用的方式,是把它当成「按症状改代码」的清单。症状相同,根因可能相反。例如「重复」既可能是去重缺失,也可能是去重过度;「不更新」既可能是连接断了,也可能是端上把消息写进了错误的对象。
三条纪律:
- 先拿证据,再改代码。 至少有一条可观察信号能区分两个根因,否则不要动手。
- 一次只改一个变量。 同时改游标、去重与刷新顺序,修好了也不知道是哪一条起作用。
- 修完补用例。 没有用例的修复会在下一次重构时复发,这是本卷反复强调测试的原因。
常见错误与边界
- 把用户描述当成根因。「列表重复」可能实际是「刷新位置错了」。
- 只看端上日志,不看服务端返回。两边的时间戳与序列号要对照。
- 用「重装应用」验证。重装会清空缓存,掩盖缓存相关的问题。
- 在弱网下不复现就认为修好了。内容类问题大多有网络相关性。
- 把降级当成修复。降级让用户可用,但没有消除根因。
- 修完不补监控。下一次同类问题仍然只能靠用户反馈发现。
- 忽略多端差异。同一账号在两端表现不同,往往说明状态同步有问题。
- 用「偶发」结束排查。偶发通常意味着时序或并发,值得继续定位。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。