一次动画冲突引发的"卡顿"
在上一篇中,我解决了 height: auto → 0 无法平滑动画的问题,本以为事情就结束了。
结果又发现了一个新问题。
当 Pool Menu(下拉菜单)处于展开状态时,如果切换到 Summary 模式,整个 Pool Selector 会先"顿一下",然后下面的内容才开始上移。
最开始我以为还是 height 或 margin-bottom 动画的问题,甚至尝试把 margin-bottom 也加入 Transition:
el.style.transition =
"height .25s ease-in, opacity .20s ease-out, margin-bottom .25s ease-in";结果是得到了一些改善,动画不再会有 margin 那一下无过度的卡顿感,整体来和展开前的动画差不多流畅。但实际上认真看还是会有些许不属于这个 Wrapper 整体渐出时会有的卡顿感。
真正的问题
后来重新梳理整个组件的结构,才发现问题并不是出在 Wrapper,而是同一个组件存在两套独立的收起动画。
Pool Selector 本身负责整个组件的显示与隐藏:
Wrapper
└── height / opacity / margin-bottom而它内部的下拉菜单(Pool Menu)也有自己的展开/收起动画:
.pool-menu {
transition: max-height .3s ease;
}正常情况下,这两个动画分别负责不同的事情:
- 点击下拉框时,由
Pool Menu负责展开和收起; - 页面切换时,由
Wrapper负责整个组件的显示和隐藏。
但当我切换到 Summary 模式时,却无意间让它们同时参与了退出动画。整个过程实际上变成了:
Pool Menu 收起(max-height) 310 → 280 → 250 → ...
Wrapper 收起(height) 68 → 60 → 52 → ...这两个动画属性有一个共同点,都会触发布局(Layout / Reflow)。
这就会导致浏览器的每一帧渲染都会为:
Layout Menu
↓
Layout Wrapper
↓
Paint也就是说,浏览器需要先处理 Pool Menu 的 max-height 变化,再处理 Wrapper 的 height Collapse。虽然这两个动画并没有真正互相阻塞,但它们都在同一个组件上修改布局,最终给人的视觉感受就是:整个组件像是"停顿了一下",然后才开始整体收起。
解决办法
既然知道了两个动画会有渲染上的布局冲突问题,那我们只需要优化掉一个就好了,但问题需要优化掉哪个呢?
后来我突然意识到一个问题:当 Wrapper 开始 Collapse 时,它本身已经在执行 height → 0,并且整个 Wrapper 还有 overflow: hidden 这个样式。
这意味着,只要 Wrapper 开始收起,里面所有内容都会一起被裁剪掉,包括已经展开的 Pool Menu。
换句话说,整个组件已经有了一个退出动画,那么 Pool Menu 再去播放一次:max-height → 0,其实已经没有意义了,它只是重复了一次 "收起" 的动作。
.pool-menu {
transition: max-height .3s ease; ←重复
}于是我直接在收起 Wrapper 之前,先关闭 Pool Menu 的 Transition:
const menu = el.querySelector(".pool-menu");
if (menu) {
menu.style.transition = "none";
menu.classList.remove("show");
}然后再执行 Wrapper 的 Collapse,这样整个退出过程就变成了:
Wrapper 收起(唯一的退出动画)
↓
Pool Menu 被 Wrapper 一起裁剪掉动画立刻变得流畅了许多,不再有之前那种 "停顿一下" 的感觉。
思考
一个组件可以拥有很多进入动画,例如:
- 下拉菜单展开
- 图标旋转
- 内容淡入
这些动画互相配合,会让组件更有层次感。但是退出动画最好只有一个"拥有者"(Animation Owner)。
对于这个组件来说:
- 用户点击下拉框时,由
Pool Menu负责展开动画。 - 页面切换、整个组件隐藏时,则完全交给
Wrapper负责收起。
不要让两个动画同时争夺同一个组件的退出过程,否则很容易出现动画冲突,最终表现出来就是一种说不出来的 "卡顿感"。
RoLingG | 博客
评论(0)