移动端列表动画不能直接用js逐项触发animation,因滚动时重排重绘易卡顿;应结合prefers-reduced-motion媒体查询降级、:nth-child()控制延迟、intersectionobserver精准触发,并重置transform/opacity以保障可访问性与性能。

移动端列表动画为什么不能直接用 animation 逐项触发
因为浏览器在滚动时频繁重排重绘,如果每个 li 都绑独立的 animation,且靠 JS 监听 scroll 判断是否进入视口,很容易卡顿——尤其低端 Android 设备上掉帧明显。真正可行的是:用 CSS 媒体查询配合 prefers-reduced-motion 控制开关,用 transform: translateY() + transition 实现位移差,而非依赖 JS 触发时机。
如何用 @media (prefers-reduced-motion) 安全降级
这个媒体查询不是“移动端专用”,但对移动端尤其关键——很多用户在设置里开了“减少动画”,系统会主动下发该偏好。不处理它,动画会照常执行,违反可访问性原则,甚至被部分浏览器强制禁用整个 @keyframes。
- 必须把动画逻辑包裹在
@media (prefers-reduced-motion: reduce)的反向规则里,例如:@media (prefers-reduced-motion: reduce) { .list-item { animation: none !important; transform: none !important; } } - 不要只写
animation: none,transform和opacity也得重置,否则残留的初始transform: translateY(20px)会导致内容错位 - 该查询在 iOS Safari 14+、Chrome 74+、Firefox 63+ 支持良好,无需 polyfill
交错位移靠 :nth-child() + transition-delay 实现
不用 JS 计算每个元素延迟时间,CSS 原生就能做。关键点在于:延迟值必须基于索引动态生成,且总延迟不能超过用户感知阈值(建议 ≤ 600ms)。
- 给列表项统一加基础过渡:
.list-item { transform: translateY(20px); opacity: 0; transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1); } - 用
:nth-child(n)逐项叠加延迟:.list-item:nth-child(1) { transition-delay: 0.08s; } .list-item:nth-child(2) { transition-delay: 0.16s; } .list-item:nth-child(3) { transition-delay: 0.24s; } /* ……最多到 :nth-child(8),再往后延迟过长,体验反而变差 */ - 进入视口后只需加一个类(如
.is-visible)来触发动画:.list-item.is-visible { transform: translateY(0); opacity: 1; }
触发时机必须用 IntersectionObserver,别用 scroll 事件
媒体查询管“开不开”,但“什么时候开始动”还得靠 JS。用 scroll 事件监听在快速滚动时会严重丢帧;而 IntersectionObserver 是浏览器原生异步回调,兼容性已覆盖所有主流移动端浏览器(Android Browser 57+,iOS Safari 12.2+)。
- 初始化 observer 时设
threshold: 0.1,表示元素 10% 进入视口就触发,比0更早启动,减少“滚动中突然弹出”的突兀感 - 每个 item 只监听一次,触发后立刻
unobserve,避免重复回调拖慢主线程 - 务必加防抖兜底:如果页面加载极慢或网络异常,可 fallback 到
setTimeout强制在 3s 后批量添加.is-visible类
交错动画的复杂点不在写法,而在节奏控制——延迟太密看不出交错,太疏又像分批发货;prefers-reduced-motion 容易被忽略,但一旦漏掉,就会在特定设备上彻底失效,且无法通过常规测试发现。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











