CSS Transitions性能优化实战技巧分享
又翻车了,transition动画卡成PPT
今天上线前最后测一遍动效,结果在安卓低端机上直接裂开——一个简单的展开收起 transition 动画,愣是卡出了幻灯片切换的效果。我寻思这都 2024 年了,CSS transition 还能翻车?可现实啪啪打脸。
需求其实贼简单:点击按钮展开一个面板,高度从 0 变成 auto,加个 0.3s 的 ease-out 过渡。代码写得那叫一个行云流水:
.panel {
overflow: hidden;
height: 0;
transition: height 0.3s ease-out;
}
.panel.expanded {
height: auto;
}
看起来没问题对吧?但一运行就发现,根本没动画,直接“啪”一下弹出来。后来查资料才知道,CSS 无法对 height: auto 做过渡,因为浏览器没法计算从 0 到 auto 的中间值。transition 只能在两个确定的数值之间插值,而 auto 是个“你看着办”的值,插不了。
试了三套方案,前两套纯属浪费时间
第一反应是用 max-height 代替 height。网上一堆这种方案,说设个超大的 max-height,比如 999px 或者 1000vh,然后过渡它:
.panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.panel.expanded {
max-height: 1000px;
}
乍一看能动,但问题一堆:内容高度不固定,设 1000px 太死板,万一内容超过呢?设 1000vh 又会导致动画时间变长(因为是从 0 到一整个视口),而且滚动条会提前出现,体验很怪。
第二套方案是 JS 读 offsetHeight 再 setTransition,听着挺靠谱:
function togglePanel(panel) {
if (panel.classList.contains('expanded')) {
panel.style.height = panel.scrollHeight + 'px';
// 强制重排
panel.offsetHeight;
panel.style.height = '0';
panel.classList.remove('expanded');
} else {
panel.classList.add('expanded');
// 强制重排
panel.offsetHeight;
panel.style.height = panel.scrollHeight + 'px';
}
}
配合 CSS:
.panel {
height: 0;
overflow: hidden;
transition: height 0.3s ease-out;
}
原理是利用 offsetHeight 触发重排,让浏览器先记住当前尺寸,再设置目标高度触发 transition。理论上可行,但我这边用了 Vue,组件一多,生命周期和异步更新搅在一起,经常拿不到正确的 scrollHeight,有时候高度假了,有时候干脆不动。折腾了一个小时,心态炸了。
最终解法:ResizeObserver + requestAnimationFrame
后来灵光一闪,想起之前看过的 ResizeObserver API。这玩意儿能监听元素尺寸变化,还不影响性能。思路是:不用 transition 控制 height,而是用 JS 监听展开状态,动态设置精确 height,并配合 RAF 做平滑过渡。
核心逻辑是这样的:状态改变时,先把 height 设为当前实际高度(瞬间完成),然后在下一次渲染前改为目标高度,这样 transition 就能正常工作。
class SmoothCollapse {
constructor(element) {
this.element = element;
this.observer = new ResizeObserver(() => this.onResize());
this.observer.observe(this.element);
}
onResize() {
const computed = window.getComputedStyle(this.element);
const currentHeight = parseFloat(computed.height);
// 如果是 auto 状态,我们手动 fix 为具体数值
if (computed.overflow === 'hidden' && !this.isAnimating) {
this.element.style.height = currentHeight + 'px';
}
}
expand() {
this.isAnimating = true;
this.element.style.height = 'auto';
const targetHeight = this.element.scrollHeight;
this.element.style.height = '0px';
// 强制重排
this.element.offsetHeight;
// 启动过渡
this.element.style.transition = 'height 0.3s ease-out';
this.element.style.height = targetHeight + 'px';
// 过渡结束后恢复 auto
setTimeout(() => {
this.element.style.height = 'auto';
this.element.style.transition = '';
this.isAnimating = false;
}, 300);
}
collapse() {
this.isAnimating = true;
const currentHeight = this.element.scrollHeight;
this.element.style.height = currentHeight + 'px';
// 强制重排
this.element.offsetHeight;
this.element.style.transition = 'height 0.3s ease-out';
this.element.style.height = '0px';
setTimeout(() => {
this.element.style.transition = '';
this.isAnimating = false;
}, 300);
}
destroy() {
this.observer.disconnect();
}
}
然后在 Vue 里这么用:
mounted() {
this.collapse = new SmoothCollapse(this.$refs.panel);
},
methods: {
toggle() {
if (this.expanded) {
this.collapse.collapse();
this.$refs.panel.classList.remove('expanded');
} else {
this.$refs.panel.classList.add('expanded');
this.$nextTick(() => {
this.collapse.expand();
});
}
this.expanded = !this.expanded;
}
}
HTML 长这样:
<div ref="panel" class="panel">
<p>这里是动态内容</p>
<p>可能有一堆文字或者组件</p>
</div>
<button @click="toggle">切换</button>
这套方案亲测在安卓 8 的微信 WebView 里也能流畅跑,算是目前最稳的解法。虽然代码多了点,但至少不翻车。
这里注意,我踩过好几次坑
- 忘记
offsetHeight触发重排,导致 transition 不生效 —— 这一步必须有,不然浏览器会合并样式操作,动画就没了 - 没清空 transition,导致后续其他动画也带上这个属性
- 在 collapse 之后没及时移除 expanded 类,影响后续判断
还有一个小问题:快速连续点击时,动画会抽搐。理论上可以用一个 isAnimating 标志位锁住,但我这边业务场景不太可能出现高频点击,就懒得加了。真要搞的话,可以结合 AbortController 或 Promise 队列控制,但成本有点高,权衡之下放了。
为啥不用第三方库?
不是没想过用 vue-transition-group 或者 animate.css,但项目已经快上线了,引入新依赖风险大。而且这种基础动效,自己写反而更可控。再说了,现在哪个前端没写过几段手搓动画的黑历史?
顺带一提,如果只是 opacity 或 transform 类的动画,原生 transition 完全够用,性能也好。真正容易出事的就是 height、margin、padding 这种涉及 layout 的属性,一动就 reflow,低端机直接卡住。
总结一下
以上是我踩坑后的总结。说白了,CSS transition 在处理非确定尺寸时就是有局限,想做到真正流畅的展开收起,还是得 JS 出马。这套方案虽然不能 100% 完美(比如快速点击会有瑕疵),但在绝大多数场景下够用了。
如果你有更好的实现方式,比如用 Web Animations API 或者 CSS Houdini,欢迎评论区交流。我现在是真不想碰这块了,刚修完又得去盯另一个页面的 z-index 层叠问题……前端这行,真是修不完的 bug,踩不完的坑。

暂无评论