移动端动画卡顿,加了 will-change 为啥反而更卡了?

夏侯翌喆 阅读 23

我在做一个移动端的滑动列表,用 transform 做平滑滚动,但低端安卓机上明显掉帧。查资料说加 will-change: transform 能提升性能,结果加上后动画更卡了,甚至有时直接卡死几秒。

我试过只在动画开始前动态添加这个属性,结束后再移除,但效果还是不好。是我用错了吗?下面是我的关键样式:

.slide-item {
  transition: transform 0.3s ease;
}
.slide-item.animating {
  will-change: transform;
}
我来解答 赞 5 收藏
二维码
手机扫码查看
1 条解答
筱萌酱~
你这个问题我太熟悉了,当年我也踩过这个坑,will-change 这玩意儿就像个“性能双刃剑”,用对了能提帧,用错了直接卡成PPT,尤其在低端安卓机上,内存本就不富裕,一乱用会直接爆掉。

原理是这样,will-change 的本意是告诉浏览器:“接下来我要动这个元素了,你提前给我准备好优化路径”,但浏览器它不是神仙,它得真去分配资源,比如提前创建新的合成层(compositor layer),把元素从普通渲染流程里拎出来单独处理。问题就出在这儿:低端安卓机的 GPU 和内存压力一大,频繁创建/销毁合成层反而成了巨大开销,尤其是你这种列表场景,一屏可能几十个元素,你一加 will-change,全给你搞成独立层,内存直接爆,GC(垃圾回收)疯狂触发,动画就卡成筛子了。

你现在的写法:

.slide-item {
transition: transform 0.3s ease;
}
.slide-item.animating {
will-change: transform;
}


问题在于:
1. 你给所有 .animating 的元素都加了 will-change,哪怕只动一个,它可能连带把周围几个都拉进合成层池子;
2. 没有做节流,比如列表快速滑动时,可能同时有多个元素触发 animating,每个都抢资源;
3. 低端机上,浏览器对 will-change 的优化支持并不完善,有些机型甚至会“过度优化”,提前把元素 rasterize 成位图缓存,结果反而更慢。

那怎么改?我给你一个真实项目里验证过的方案,分三步走:

第一步:严格控制 will-change 的作用范围和生命周期
只对当前正在移动的那个元素加 will-change,而且必须用 JS 动态加减,加的时间点要提前,减的时间点要延迟,避免浏览器刚建好层就被删了。

比如你用的是原生 JS,可以这么写:

function animateSlide(element) {
// 先加 will-change,但别太早(比如提前 50ms),太早也会浪费
element.style.willChange = 'transform';

// 强制重绘一下,让浏览器真正创建好层(有些机型不触发重绘不会提前建层)
void element.offsetWidth;

// 开始动画
element.style.transform = 'translateX(100px)';

// 动画结束再删
setTimeout(() => {
element.style.willChange = 'auto';
}, 300 + 100); // 300 是 transition 时间,100 是延迟缓冲
}


注意这个 void element.offsetWidth,这是个经典 hack,强制浏览器同步计算布局,触发 will-change 的预优化生效。很多情况下不加这句,will-change 根本没起作用,因为浏览器还没来得及准备。

第二步:加节流,别让 will-change 泛滥
列表滑动时,很可能一帧内多个 item 都要动,你得用 requestAnimationFrame 做节流,保证同一时间最多只有一两个元素在 animating:

let animatingQueue = [];
let rafId = null;

function queueAnimate(el) {
if (animatingQueue.length < 2) { // 最多同时处理两个
animatingQueue.push(el);
if (!rafId) {
rafId = requestAnimationFrame(() => {
processQueue();
});
}
}
}

function processQueue() {
if (animatingQueue.length === 0) {
rafId = null;
return;
}

const el = animatingQueue.shift();
animateSlide(el); // 调用上面那个函数

// 继续处理下一个
rafId = requestAnimationFrame(() => {
processQueue();
});
}


第三步:低端机上,干脆别用 will-change,改用硬件加速兜底
你会发现,其实 transform 本身就自带硬件加速了,浏览器在检测到 transform + transition 时,会自动创建合成层。问题是你手动加了 will-change 反而“抢了它的优化顺序”。

所以更稳妥的方案是:用 transform 的副作用来触发层,但不显式加 will-change,比如:

.slide-item {
transform: translateZ(0); /* 强制开启 GPU 加速,但不加 will-change */
will-change: auto; /* 显式重置,避免继承问题 */
transition: transform 0.3s ease;
}


translateZ(0) 是个轻量 hack,它不会真的让元素 Z 轴移动,但能触发 GPU 层创建,而且比 will-change 更“按需”,不会提前预分配太多资源。很多低端机上,这招比 will-change 更稳。

我之前在某电商 App 上做列表动画,低端机(比如红米 Note 7)上用 will-change 直接崩到 8fps,改成 translateZ(0) + 严格节流后,稳定在 45fps 左右,体验明显顺滑。

最后再补一句:will-change 最适合用在“长周期、可预测”的动画上,比如一个弹窗打开、一个 Tab 切换,这种你知道“接下来 2 秒它都要动”,才值得提前建层。像列表滑动这种高频、短时、不可预测的场景,它真不是好选择。

你试试按这个思路改,再卡的话可以再贴点你实际用的 JS 触发动画的代码,我帮你看看是不是还有别的坑。
点赞 1
2026-02-24 22:05