首屏接口要 1.8 秒才回,页面一直是白的,用户以为没点动,连点三次提交。后来加了个转圈,重复提交没了,跳出率却没怎么降。

border 旋转是最轻的实现

一个 32 像素的方块,两条边透明、两条边有色,加 animation 每秒转一圈,浏览器只做合成不重排。写在样式里几十个字节,不用图片也不用框架。边框宽度按 4 的倍数给,缩放时不会糊。转太快像卡住,0.8 到 1 秒一圈比较耐看。

骨架屏比转圈少一次跳动

列表页用灰色条块先占住真实内容的位置,数据回来直接替换,视觉上不跳。条块高度对齐最终行高,圆角跟卡片一致,用户能预判布局。首屏有图的位置留一个同比例的灰块。骨架屏别画太细,否则替换瞬间会有明显的重排。

动画结束要真的移除

数据到了只把 loading 隐藏,节点还挂在 DOM 上,动画继续跑,低端机白白耗电。直接删掉节点,或者暂停动画。定时器也要清,不然接口报错时动画永远停不下来。多个请求并发时,用一个计数器记还没回来的数量,归零才收起。

系统开了减少动态就降级

在媒体查询里判断 prefers-reduced-motion,命中时把旋转换成静态文字或者淡入,避免前庭敏感的人看着晕。iOS 的低电量模式也会影响动画帧率,别把加载完成绑在动画结束事件上,那是两回事。

按钮里的转圈顺手防重复提交

提交时把按钮置成 disabled,同时挂上 aria-busy,转圈画在按钮内部,宽度靠 padding 撑住,别让按钮突然缩窄。请求失败要把状态改回来,只换文案不改 disabled,用户点第二次照样没反应。再加 300 毫秒的节流,比拦住所有点击体验好。

动画只碰 transform 和 opacity。改 width、top、margin 会让浏览器每帧重排,列表里几十个占位块一起动,低端安卓直接掉到二十几帧。确实要变尺寸,用 scale,或者提前把容器高度写死。

加载动画的作用是告诉用户还在等,不是装饰。先保证能撤干净,再谈好不好看。