prefers-reduced-motion 是唯一被 wcag 认证、全平台稳定支持的动画控制开关,应直接用纯 css 响应,避免 js 动态加 class 导致 fouc 和兼容问题;动画仅安全使用 transform 和 opacity,关键帧精简为 0%→100%,will-change 需动态设置并及时清除,小屏设备需降级复杂动效。

为什么直接用 prefers-reduced-motion 就够了
它不是“可选方案”,而是唯一被 WCAG 认证、所有现代浏览器(包括 Android WebView 5.0+ 和 iOS Safari 13+)稳定支持的语义化开关。用户主动关闭动画,比你猜设备性能更可靠——很多低端安卓机默认开着动画但一跑就卡死,而部分高端机用户反而手动关了动画。
错误做法是用 JS 去读 window.matchMedia 再动态加 class,这会导致 FOUC(闪动)、SSR 不一致、旧版微信 WebView 报错,且监听不及时。正确写法就是纯 CSS:
@media (prefers-reduced-motion: reduce) {
.card, .button, .loading-spinner {
animation: none !important;
transition: none !important;
}
/* 注意:必须重置 transform/opacity,否则残留位移会错位 */
.is-sliding {
transform: none;
opacity: 1;
}
}
这个规则必须放在所有动画样式之后,用 !important 确保覆盖力。iOS Safari 13 以下会忽略该查询,属于安全降级(不报错、不生效),不用额外兜底。
transform 和 opacity 是仅有的安全动画属性
只要关键帧里混进一个 width、background-color、box-shadow 或 margin,整段动画就退回到 CPU 渲染,低端机帧率会从 60fps 直接掉到 20fps 以下——浏览器不会报错,但你会看到幻灯片式卡顿。
安全写法只保留两个属性:
-
transform: translateX(100px)替代left: 100px -
transform: scale(0.9)替代width: 90% -
opacity: 0.5替代color: rgba(0,0,0,0.5)(文字颜色渐变请用两层叠盖 + opacity 切换) - 关键帧尽量压缩为
0%→100%,删掉中间冗余节点
示例:
@keyframes slide-in {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
will-change 必须动态设置并立刻清除
静态写 .item { will-change: transform; } 是在低端安卓机上制造 OOM 的最快方式——每个元素都提前占一块 GPU 显存,一屏 20 个列表项,内存直接爆掉。
真正有效的做法是:仅对「即将开始动画」的单个元素,在用户真实交互瞬间(如 touchstart)设置,动画一结束立刻清空:
- JS 添加:
el.style.willChange = 'transform'; - 监听
animationend(不是transitionend),然后设el.style.willChange = 'auto'; - iOS Safari 对 ≤16ms 动画可能不触发
animationend,加兜底:setTimeout(() => el.style.willChange = 'auto', duration + 100);
别对整个列表批量操作,也别在 CSS 里写死 will-change。
小屏设备要主动压缩动画复杂度
分辨率 ≤480px 或 max-resolution: 1.5dppx 的设备,大概率是旧款小屏安卓机(如 Galaxy S5、Moto G4),它们的 WebView 调度能力弱,哪怕只是多一个 transform-origin 或 cubic-bezier 插值,都可能让合成线程卡住。
建议在媒体查询中做粗粒度降级:
- 禁用逐字飞入、交错延迟等高负载效果,改用统一淡入:
animation: fadeIn 0.6s ease; - 旋转类 loading 图标必须显式写
transform-origin: center;,否则 iOS 12–14 WebKit 会中途跳回 0 度 - 避免
from {} + to {}以外的关键帧语法,旧版 Safari 解析异常风险高 - 图片懒加载动效要防 iOS Safari “唤醒延迟”:DOM 加载后延迟 16ms 主动调用
observer.takeRecords()
最常被忽略的掉帧元凶,是在动画过程中调了一次 getBoundingClientRect() 或 offsetHeight——哪怕只是读取,也会强制同步 layout,直接打断渲染流水线。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











