点导航,却切了主题:一次改版的两个事故
顶部导航上线当晚就出了两个事故:先是谁都点不准导航,第二天滚动又「不丝滑」了。排查下来,两个问题都不在新写的代码里——新代码没写错,错的是改版动掉了旧代码赖以成立的前提。把两次排查记在这里。
事故一:点「笔记」,却切换了主题
现象很直接:点导航里的「笔记」「项目」「关于」,触发的全是右上角的主题开关。
先用 elementFromPoint 对四个链接的中心逐点探测——命中的全部是同一个 BUTTON.theme-btn。但按钮矩形在 x≈990、宽约 90,链接在 753–976,并不重叠。挡路的不是按钮本体,是某个更大的东西。把按钮的 ::after 伪元素一量:
| 对象 | 实测尺寸 | 说明 |
|---|---|---|
| 按钮本体 | 约 90 × 40 | 正常,与链接无重叠 |
.theme-btn::after | 828 × 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 带上限截断,防止标签页切回来时一帧追完。改完在任何刷新率下手感一致。
收尾:两张检查单
两个事故共用的教训:布局改版改掉的是「前提」,不是「代码」。热区层的定位锚、平滑模块的运行条件,都是写代码时成立、改版后失效的假设。往后按这两张单子验收:
改版验收单
- 用
elementFromPoint扫一遍关键热区——伪元素不进getBoundingClientRect,截图也照不出来,只有命中测试能暴露它; - 至少一次真实 click 走完路由,不能只看样式;
- 宿主
position变更时,检查其::after热区的参照物是否换了; - 在「减少动效」开、关两种状态下各验一遍交互。
「主观卡顿」分流单
- 先量帧间隔——掉帧才是渲染问题;
- 再查
matchMedia门控——必须在报障人的真实环境查,开发机查不出来; - 最后才看
backdrop-filter这类半透明合成的固定开销。