真正能稳跑的方案只有 transform: scaleX() 或 background-size 驱动动画,全程避开重排;width 动画因触发 layout 导致卡顿、回弹甚至不渲染,是浏览器底层机制限制。

直接用 width + transition 或 @keyframes 控制宽度,在移动端大概率会卡顿、跳帧,尤其在低端 Android WebView 和 iOS Safari 旧版本里。真正能稳跑的方案只有一条路:用 transform: scaleX() 或 background-size 驱动动画,全程避开重排。
为什么不能用 width 动画
移动端浏览器对 width 变更的响应需要触发 layout → paint → composite 三步,其中 layout 是主线程同步操作,JS 频繁更新(比如轮询进度)极易被节流或丢帧。真机测试中常见现象包括:
-
progress-bar突然“回弹”到 0% - 动画跑到一半卡住,几秒后才突然拉满
- 在 Android 4.4–6.0 的系统 WebView 中完全不渲染
这不是写法问题,是浏览器底层机制限制——width 不走合成层,GPU 不加速。
推荐方案:transform: scaleX() + JS 控制
这是目前兼容性最好、性能最稳的实现,iOS 9+、Android 4.4+ 全支持,且代码轻量。关键点不是“怎么写动画”,而是“怎么和 JS 协作”:
立即学习“前端免费学习笔记(深入)”;
- 进度容器设固定宽高,内部填充元素设
width: 100%、transform-origin: left center - JS 更新时只改
element.style.transform = 'scaleX(' + ratio + ')',别碰width或left - 动画用
transition: transform 300ms ease-out,别用linear——真实加载不是匀速的,开头快、结尾慢更符合用户预期 - 如果要手动中断(比如加载出错),直接设
transform: scaleX(0.8)即可,无需清 animation
示例 CSS:
.progress-bar { height: 0.2vh; background: #e0e0e0; }
.progress-fill { height: 100%; background: #4a90e2; width: 100%; transform-origin: left center; transition: transform 300ms ease-out; }
纯 CSS 循环动画:background-size 方案
如果只是做“假加载”(比如首屏骨架屏过渡),不需要 JS 接入,用 background-size + @keyframes 最省事,且不会因 JS 延迟导致首帧空白:
- 用
linear-gradient定义渐变色,再通过background-size: 0% 100%→100% 100%拉伸模拟填充 - 动画时长设
2.5s,比多数资源加载耗时略长,避免“条跑完了图还没出来” - 必须加
forwards,否则动画结束瞬间会闪回 0% - 慎用
will-change: background-size——它在 Safari 上可能引发内存泄漏,仅在动画启动前临时加,结束后立刻移除
示例片段:
@keyframes progress-fill {
from { background-size: 0% 100%; }
to { background-size: 100% 100%; }
}
.progress-bar {
height: 0.15vh;
background: linear-gradient(to right, #4a90e2, #4a90e2) left/0% 100% no-repeat #f0f0f0;
animation: progress-fill 2.5s linear forwards;
}
容易被忽略的适配细节
很多团队写了效果,上线后在 iPhone SE 或折叠屏上消失,问题往往出在单位和初始化上:
- 别用
px或rem设高度——用户缩放、UA 修改根字体都会让它变粗或不可见;坚持用vh,0.15vh是实测安全值 - 伪元素(如
::after)做进度条时,父容器必须有position: relative,否则在 flex 或 transform 父级里定位偏移 - 用 CSS 变量(如
--progress)配合 JS 时,记得首次设置后调用getComputedStyle强制触发重绘,否则某些安卓机型首帧不生效 - 所有动画都加
will-change: transform是反模式——只对正在动画的元素加,且 JS 动画结束后必须el.style.willChange = 'auto'


















