嵌入式开发 · 传感器应用 · 物联网通信 · 学习与作品记录

← 返回笔记列表

点导航,却切了主题:一次改版的两个事故

顶部导航上线当晚就出了两个事故:先是谁都点不准导航,第二天滚动又「不丝滑」了。排查下来,两个问题都不在新写的代码里——新代码没写错,错的是改版动掉了旧代码赖以成立的前提。把两次排查记在这里。

事故一:点「笔记」,却切换了主题

现象很直接:点导航里的「笔记」「项目」「关于」,触发的全是右上角的主题开关。

先用 elementFromPoint 对四个链接的中心逐点探测——命中的全部是同一个 BUTTON.theme-btn。但按钮矩形在 x≈990、宽约 90,链接在 753–976,并不重叠。挡路的不是按钮本体,是某个更大的东西。把按钮的 ::after 伪元素一量:

对象实测尺寸说明
按钮本体约 90 × 40正常,与链接无重叠
.theme-btn::after828 × 79横贯整条导航的隐形点击层

根因是一行看似无害的改版样式:

版本写法效果
旧版.theme-btn{position:absolute} + ::after{inset:-9px}热区锚在按钮自身,44×44 的触屏放大层
改版条内写成 .topbar .theme-btn{position:static}定位上下文消失,::after 就近锚到 sticky 顶栏,变成 828×79

关键在于:::after 用 inset 定位时,参照的是最近的定位祖先。按钮自己不再定位,参照物就换成了 sticky 的顶栏——热区没变大,是「搬家」了。

修复相应地简单:按钮改回 position:relative,条内热区收成 inset:-6px(102×52,仍满足 44×44 的触控下限,且与最近的链接留出 8px 间隔)。

事故二:滚动忽然不丝滑

第二天用户报「滚动一格一跳」。直觉是渲染掉帧,但本地探针跑了 153 帧,帧间隔 p50 6.9ms、p95 7.0ms,最长 7.1ms——零掉帧,渲染没有任何问题。

那问题就在行为层:平滑滚动模块的第一行是 if (rm.matches) return,其中 rm = matchMedia('(prefers-reduced-motion: reduce)')。这台 Windows 在系统设置里关闭了「动画效果」,该查询恒为 true——模块从上线第一天起就没运行过,用户看到的一直是浏览器原生滚轮,一格一跳。

「减少动效」的本意是豁免装饰性动画。滚轮平滑算不算装饰?最终口径:它是输入跟随,和光标移动同类,不是装饰——于是删掉这行门控,保留触屏豁免(hover:none)。

顺手把缓动也修了:原来每帧固定 0.115 的追赶系数,帧率越高追得越快,60Hz 和 144Hz 手感不一致。换成时间常数:

var dt = lastT ? Math.min(now - lastT, 100) : 16.7;
current += d * (1 - Math.exp(-dt * 0.009));   /* τ ≈ 111 ms */

dt 带上限截断,防止标签页切回来时一帧追完。改完在任何刷新率下手感一致。

收尾:两张检查单

两个事故共用的教训:布局改版改掉的是「前提」,不是「代码」。热区层的定位锚、平滑模块的运行条件,都是写代码时成立、改版后失效的假设。往后按这两张单子验收:

改版验收单

  1. 用 elementFromPoint 扫一遍关键热区——伪元素不进 getBoundingClientRect,截图也照不出来,只有命中测试能暴露它;
  2. 至少一次真实 click 走完路由,不能只看样式;
  3. 宿主 position 变更时,检查其 ::after 热区的参照物是否换了;
  4. 在「减少动效」开、关两种状态下各验一遍交互。

「主观卡顿」分流单

  1. 先量帧间隔——掉帧才是渲染问题;
  2. 再查 matchMedia 门控——必须在报障人的真实环境查,开发机查不出来;
  3. 最后才看 backdrop-filter 这类半透明合成的固定开销。